Self-cleaning compile cache, concurrency and IPC hardening.
- The compile cache cleans itself up. Every distinct script version left an
output dir behind forever (one machine: 466 entries, 439 MB in five weeks). The
daemon now tracks when each entry was created and last hit and sweeps hourly:
entries unused for 24 hours are deleted, an entry that comes back within 30 days
(a weekly job) is promoted and kept until it goes 30 days unused, and running
scripts keep their entry alive with a 10-minute ping from
sea/the host. The sweep also skips entries whose files a running process has loaded. - Concurrent compiles in the daemon are safe. Scripts compiled at the same
time shared per-request state: two scripts with different
//sea_incdirdirectories could resolve each other's same-named include, and concurrent first runs of one script could be handed its output directory before the NuGet assets and manifest were staged. Include directories are now local to each resolution, the NuGet resolver cache is concurrent, compiles of the same script are serialized, and an output directory only counts as a cache hit once a completion marker is written last — which also repairs entries left half-staged by a crash. - Concurrent elevated scripts no longer cross responses. The daemon now runs one elevator exchange at a time (replies carry no request id). Queueing and the reply are both bounded, and the CLI stamps each request with a forward-by deadline, so a request is never started after the CLI gave up. The CLI's start-the-elevator-and-retry path now only retries failures that provably never reached an elevator, so an elevated script cannot start twice. A new elevator is acknowledged before it is used, and a failed exchange no longer discards an elevator that re-registered meanwhile.
- No more unbounded IPC waits. Status, stop, daemon discovery and elevated spawns now time out when the peer accepts the connection but never replies, and closing a connection after a cancelled send no longer blocks on flushing to a peer that stopped reading; watch sessions end as soon as the CLI disconnects instead of on the next file change, so they no longer block the daemon's idle shutdown. A compile no longer runs on the daemon's accept loop, where it could keep other clients from connecting until it finished.
- Linux scripts get the real implementation of RID-specific packages. The
RID fallback chain stopped at the OS name and never reached
unix, so packages whose Linux build ships underruntimes/unix/lib/(e.g. Microsoft.Data.SqlClient) fell back to thelib/stub that throwsPlatformNotSupportedException. Selection now walks the portable RID graph the way NuGet does (linux-musl-x64 → linux-musl → linux-x64 → linux → unix-x64 → unix → any), uses the process architecture, accepts versionedlibfoo.so.Nnatives, and logs flat-staging filename collisions instead of dropping the second file silently. New end-to-end SQLite and SqlClient tests run on all three CI platforms. - SeaShell.Host depends on stable Microsoft.Extensions.Hosting 10.0.12 instead of the 10.0.0-preview.5 packages.
- CI runs the unit tests again.
dotnet test --no-buildon a fresh checkout skipped restore and silently ran nothing, and the Windowsseawtests probed a pre-0.5.0 path and skipped. Both now run and fail loudly. - A script's leftover child no longer holds a ScriptHost run open. A child
process inherits the script's captured stdout/stderr, and
RunAsyncwaited for those to close: a run whose script had exited, or whose token had fired, returned only once every such child was gone — in one case 15 minutes past a 60-second timeout — and nothing stopped the child. Output is now read as it arrives and collected for at most two seconds after the script exits. Cancelling a run asks the script to stop, waits up to five seconds, then terminates it together with every process it started (a job object on Windows, an environment marker on Linux), also during a reload, and returns the output captured so far with the newScriptResult.Cancelledset. A child left behind by a script that exits on its own keeps running. Unit tests and an end-to-end ScriptHost test run on all three CI platforms. //sea_restartworks in watch mode. A//sea_watchscript that exited for a restart announced it, waited out the backoff, and then ended the session with exit code 0 instead of starting again. It now restarts, and the first file change after a restart is no longer lost.
NuGet dependency resolution fix for DacFx and runtime-specific assets.
- Direct
//sea_nugetversions now win over transitives. Multiple package directives are resolved as one graph, preserving a direct pin such asSystem.Runtime.Caching 8.0.1even when a dependency requests an older floor. - Runtime asset selection avoids
netstandardshims when real net6+ assets exist. RID-specific managed assets and genericlib/net6.0+assets are preferred over compatibility throw-shims for a net10 host. - Unsupported NuGet directive diagnostics.
#r "nuget: ..."and#:packagenow report a SeaShell-specific message pointing users to//sea_nuget <Package> [version]. - Stale installers no longer downgrade scheduled runtimes. When a target's
currentjunction points to a newer version, install exits as a no-op before stopping processes or changing tasks. This prevents a split SYSTEM install with a newer junction but an older boot task/daemon. SYSTEM verification failures now point toseashell status --system, whose output includes the SYSTEMcurrentjunction explicitly and warns when its version differs from the registered daemon task. - Native runtime assets load. Packages carrying
runtimes/<rid>/native/*(SqlClient SNI, DacFx, SQLite, SkiaSharp, …) threwDllNotFoundExceptionat first use: native files were staged and declared at their package-relative subpath while every library istype:"project", whose assets the host resolves against the app directory root. Native assets are now copied flat and declared by bare filename, like the managed ones. Covered by a deps.json unit test and an end-to-end DacFx test against a real SQL Server. - IPC deserialization hardened. The daemon/client MessagePack channel now
deserializes with
MessagePackSecurity.UntrustedData(depth limits and hash-collision-resistant collections).
SID-based identity, direct elevated SYSTEM install, install --all
single-elevation setup.
- Identity is explicit and SID-based. New
SeaShellIdentitytype: the identity token in pipe/socket names, mutex names, lock names, and task XML principals is now the lowercase SID string on Windows (uid on Unix) instead ofEnvironment.UserName— canonical, locale-independent, and stable under LocalSystem. The key property is that a TARGET identity is computable from a different security context:SeaShellIdentity.LocalSystemdescribes the SYSTEM install (well-knownS-1-5-18,systemprofiledata dir) from an elevated admin console. Endpoint parsing switched from last-hyphen to first-hyphen-after-version (SIDs contain hyphens; versions never do) and validates the version segment, which also filters GUID-shaped script pipes. Pre-0.6.0 names still parse, andLegacyTokenkeeps installers able to recognize and stop old daemons during migration. --systeminstall runs directly in the elevated bootstrapper. The entire transient-task machinery is gone:SystemOps,SystemOpRunner, the cmd.exe wrapper templates, nupkg tool-store probing,ProgramDatastaging, and the 300s sentinel polling. Registering anS-1-5-18boot-trigger task only requires elevation, and with explicit identity every other step (extract, markers, junction) is plain file I/O into SYSTEM's profile. The daemon task is started immediately after registration, and verification waits for the daemon to write its pid/pipe markers under its own identity and answer a ping — proving the chain end-to-end. The dotnet-tool layer undersystemprofile\.dotnet\is no longer used; a legacy sweep removes the old shim/store footprint and stage dirs. SYSTEM installs no longer register an elevator (pointless for an already-privileged account), PATH entry, or file associations (those wrote to the wrong HKCU anyway).seashell install --all— one command, one UAC prompt. Composite install: user install + elevator task + Event Log source + SYSTEM install, then starts the daemon and elevator and verifies the elevator registered with the daemon (PingResponse.ElevatorVersion). From a non-elevated console it relaunches itself elevated and streams the child's output back into the original console by tailing a tee'd log file. An--invoker-sidguard detects over-the-shoulder UAC with a different account and skips the user-profile parts with guidance instead of installing into the wrong profile.- Daemon pipes grant
BUILTIN\AdministratorsReadWrite. An elevated installer can ping/stop daemons of other identities gracefully — most importantly SYSTEM's during--systemupgrades. Grants nothing without elevation (deny-only group SID in filtered tokens). - User-scope script mutex is genuinely cross-session now. The name was
unprefixed, which lands in the session-local namespace — "user" scope never
excluded across RDP/console sessions. New name:
Global\+ SID token. During 0.6.x both the new and the legacy username-based names are held (in fixed acquire order) so mixed versions exclude each other; the shim is removed in 0.7. - Install lock fixed and target-scoped. Same session-local-namespace bug
as the mutex (
Global\prefix added), and the lock name now uses the TARGET identity token, so two elevated admins racing a--systeminstall contend on one lock. - Stop flow hardened.
StopAsyncdefault timeout 2s → 10s (a daemon mid-compilation was routinely escalated to hard kill); stop now also kills by pid markers before falling back to name-based kill, and the name-based kill is skipped for the SYSTEM target (it can't distinguish owners and must not take down the admin's own daemons). - Version comparisons unified.
DaemonLaunchercompared daemon versions with ordinal string compare in two spots (the v0.4.4 bug class); nowVersion.Parse. - Junction self-heal.
IsJunction/ReadTarget/CreateOrUpdatehandle orphaned links: a danglingcurrentjunction reads as "no current version" and is deleted and recreated on install instead of being trusted. seashell status --systemreads the install directly. Versions and marker-based ping when elevated; graceful degradation to task-state and pipe-presence probing when not. No SYSTEM-context dispatch.
Side-by-side versioning, proper --system install, Installer assembly
extracted, sweeping cleanup of pre-0.5.0 layouts.
- Side-by-side per-version directories. Each installed version lives
under
{DataDir}/v{version}/with its ownbin/,cache/, and marker files. Acurrent/junction (or symlink on Linux) points at the active version; PATH always goes through the junction so it survives upgrades. Upgrading no longer replaces a single shared install — newer versions install alongside, the junction is repointed, and older versions are cleaned up only after a liveness check confirms no active clients. - Marker-based daemon/elevator discovery replaces
ServiceManifest.daemon.path/elevator.path/daemon.pipe/daemon.pidare plain text files in the version directory, written by the installer (paths) and by the daemon/elevator itself at startup (pid/pipe).BinaryDiscovery.FindDaemon(version)reads the marker and validates the target exists, with fallback to the newest installed version ≥ the requested one (covers host-embedded scenarios where the caller's pinned version may have been uninstalled). The old manifest-based address lookup, staged-binary hashing, and daemon-hash-mismatch restart loop are all gone. seashell install --systeminstalls under SYSTEM's own user profile. Previously--systemrouted to%ProgramData%\seashell\(a machine-wide location) and baked the invoking elevated user's identity into task XML + marker content. WithEnvironment.UserNamereturning{MachineName}$on .NET 10 under LocalSystem (and more exotic strings on domain-joined hosts), that baked-in identity then mismatched what the daemon reported at runtime. The new model:--systemregisters a transient one-shot scheduled task with principalS-1-5-18, which runs a plainseashell installas SYSTEM. Every identity-derived value (pipe name, task UserIds, DataDir) resolves under the same identity the daemon will later see. SYSTEM's install lands in%SystemRoot%\System32\config\systemprofile\AppData\Local\seashell\, and its daemon task uses aBootTriggerso Windows services running as SYSTEM can find a warm daemon at boot.SeaShell.Installerassembly. Task-Scheduler integration (DaemonTasks,TaskQuery), file-association registration (FileAssoc), the SYSTEM-op bootstrap (SystemOpRunner), and the pre-0.5.0 layout sweep (LegacyCleanup) all live here now, referenced by both the Bootstrapper and the CLI. Installer depends only onSeaShell.Common+SeaShell.Protocol— no Roslyn or scripting surface. The CLI's--install-daemon,--install-elevator,--uninstall-daemon,--uninstall-elevator,--associate, and--unassociateflags are removed; install lifecycle lives entirely in theseashellcommand now.- Pre-0.5.0 layout cleanup on upgrade.
seashell installdetects the flat pre-versioning layout ({DataDir}/bin/,cache/,daemon/,elevator/,seashell.json) and wipes it before creating the new version directory. Uses rename-then-delete so locked binaries on Windows can't block the upgrade — a lockedbin/is moved aside asbin.remove-{timestamp}/and cleaned up on the next install pass once the lock releases. - Post-install verification block. Every
seashell installends with a verification pass: marker files exist and point at real binaries,currentjunction resolves to the version dir,sea --versionreports the expected version, scheduled tasks are registered, file associations point at the per-version exe. Fails the install with exit code 1 if anything's off — catches the kind of silent drift where an install "succeeded" but produced a non-working tool. - Per-user install mutex.
seashell installandseashell uninstallacquire a Windows kernel mutex (SeaShell_install_u{user}) or a Linux exclusive file lock ($DataDir/install.lock) for the duration of the operation. Concurrent runs under the same identity fail fast with a clear message instead of racing on the filesystem. - Daemon hash removed.
PingResponse.DaemonHashand theComputeSelfHash/ComputeDirHashpair are gone. Side-by-side pipe names encode the version directly, so a daemon and client on the same pipe are guaranteed to match — hash-based validation was dead code. ProgramDatano longer used.SeaShellPathsalways resolves toLocalAppDataon Windows. SYSTEM'sLocalAppDatalives under its own user profile, which keeps a SYSTEM install per-user (just for the SYSTEM user) rather than machine-wide.- Installer scripts removed. The pre-0.5.0
install.ps1andinstall.shcurl/iex one-liners are gone —dotnet tool install -g SeaShellis now the only install path. Offline / archive-based installs use--add-source <dir>on the same command.
Runtime daemon discovery, install cleanup, locked-file install robustness.
daemon.addressremoved from manifest — runtime enumeration replaces install-time prediction. Thedaemon.addressfield inseashell.jsonwas written by the installer using its ownEnvironment.UserName, which diverges from the daemon's runtime identity when the two run under different principals — notably on .NET 10 under LocalSystem, whereEnvironment.UserNamereturns{MachineName}$, not"SYSTEM". The field is deleted fromComponentEntry(System.Text.Json's defaultUnmappedMemberHandling=Skipsilently drops it on read, so existing manifests migrate automatically) and replaced with a newTransportEndpoint.EnumerateDaemonEndpoints(identity)helper that enumerates live named pipes on Windows (\\.\pipe\seashell-*) or socket files on Linux ($XDG_RUNTIME_DIR/seashell-*.sockwithPath.GetTempPathfallback).ServiceManifest.GetCompatibleDaemonAddressesnow uses the enumeration helper directly and no longer consults the manifest for address lookup.DaemonLauncher.EnsureRunningAsync's contract is unchanged.- Bootstrapper stale-version cleanup loop removed — installs are additive.
The loop probed manifest-stored addresses to decide whether to delete old staged daemon binaries
at install time. Combined with
IsSocketRespondingon Windows returningfalseunconditionally, the loop routinely concluded "daemon not running" and deleted staged DLLs while a live daemon was loading them. Old{dataDir}/daemon/<hash>/dirs now linger across upgrades. A safe explicit-uninstall replacement is tracked as a follow-up alongside--systemscheduled-task registration and the matching identity capture inScheduledTasks.cs, which share the same identity-mismatch root cause and need to be designed cohesively. seashell status --systemdiagnostic. New command that enumerates pipes under\\.\pipe\seashell-*, filters out the current principal's own pipes, and probes each remaining pipe withNamedPipeClientStream.Connect. Classifies each asreachable/present (ACL-blocked)/not running, with a gsudo hint when anything is ACL-blocked. Identity-agnostic by design — it reports whatever is actually on the filesystem rather than hardcoding an expected identity string.- Stamped
.old.<ticks>suffix for install rename-on-write. When the installer encounters a locked file, it renames the existing copy aside and extracts the new one in place. The rename suffix was a fixed.old, which collided if the prior install's.oldfile was still held open by a running process — causing the second install to crash withIOException: Cannot create a file when that file already exists. The suffix now includesDateTime.UtcNow.Ticksso each install's rename targets are unique per-install. The sweep that cleans up stamped.old.*files at the start of the next install is recursive and best-effort.
Fix named-pipe accept-loop race on Windows.
- Listener pool —
TransportServeron Windows used to create exactly oneNamedPipeServerStreaminstance perAcceptAsynccall, leaving a window with zero listening instances between accept-loop iterations. Clients probing in that window hitERROR_FILE_NOT_FOUND(surfaced as Access Denied by the OS to unprivileged callers), causing intermittent health-check failures during daemon startup bursts. The server now holds a pool of pre-created concurrently-pending accept tasks (4 on Windows; 1 on Linux, where the Unix domain socket + kernel backlog already absorb bursts) and refills synchronously after each successful accept, so at least N-1 listeners remain live while any connection handler runs. PipeSecuritylifetime — Built once in theTransportServerconstructor and shared across all pool instances. Mismatched security across instances of the same pipe name would otherwise throw fromCreate.AcceptAsync/ accept loop —AcceptAsyncgains an optionalmaxWaitparameter that returnsnullon expiry without disturbing the pool. DaemonServer's two accept-loop branches (always-on and idle-timeout) collapse into a single shape — idle polling no longer needs a linkedCancellationTokenSourceplus anOperationCanceledExceptionround-trip.
Install rename-on-write fallback on Linux.
- Bootstrapper — The locked-file fallback (rename
.old, extract new) was guarded byisWindows. On Linux,File.Moveworks on a running binary (inode stays) andFile.Deleteof the.oldsucceeds, so the same strategy applies. Removed the guard.
System-wide install, version sorting fix.
seashell install --system— System-wide install for SYSTEM/root. Installs to%ProgramData%\seashell\bin(Windows) or the root user's standard location (Linux). Uses machine PATH instead of user PATH. Skips Task Scheduler registration — daemon starts lazily on firstseainvocation. Requires elevation (gsudo/sudo).seashell uninstall --system— Reverses system install.- Version string sorting fix —
NuGetResolver.PickLatestVersion(),NuGetUpdater.GetLatestCachedVersion(),ServiceManifest.GetCompatibleDaemonAddresses(), and the bootstrapper status display now useVersion.Parse()instead of lexicographic string comparison. Fixes incorrect version ordering when segment values cross digit boundaries (e.g., v49 > v50 under string sort).
Per-script apphosts, console title, status improvements.
- Per-script apphost —
AppHostWritergenerates a native executable per script instead of usingdotnet exec. Scripts launch instantly as native processes with correct process names in task manager. - Console title —
seasets the console title to{scriptName} — SeaShell v{version}while a script is running. seashell statusimprovements — Shows installed versions from the service manifest with daemon/elevator health status. Detects and reports broken staging directories.
Compatible daemon discovery.
- DaemonLauncher version fallback — When the exact-version daemon isn't running,
DaemonLauncherqueries the service manifest for newer compatible versions and probes their addresses. Scripts can run against a higher-version daemon without restarting. Eliminates unnecessary daemon restarts during development when the version bumps but the protocol is compatible.
Gitea Actions CI, bootstrapper improvements, bug fixes.
- Gitea Actions — Full CI/CD pipeline on self-hosted Gitea instance, replacing the custom pipeline (orchestrator + Unison + worker.cs). Three-runner matrix: win-x64, linux-x64, linux-musl-x64. Jobs: version → build (3×) → package → test. Version counter bumped automatically via Gitea API.
seashell schedule/seashell unschedule— Bootstrapper subcommands for managing scheduled script execution via Task Scheduler (Windows).
- Bootstrapper DataDir for SYSTEM —
GetDataDir()detects SYSTEM/LocalService/ NetworkService SIDs and uses%ProgramData%\seashellinstead of%LOCALAPPDATA%. - Daemon staging ownership — Bootstrapper writes the service manifest during install; Invoker reads it. Eliminates race between staging and manifest updates.
- seaw error reporting — Uses
GetConsoleWindow()instead ofGetConsoleProcessList()for console detection. Compilation errors logged to Event Log when no console is available. - windowMode passthrough —
windowModeparameter propagated through allScriptInitpaths. Previously missing from some code paths, causing incorrect console behavior. - OpenRC/sysvinit service start — Init scripts stripped of CRLF line endings.
/sbinadded to PATH before invoking OpenRC/sysvinit commands on Alpine. - MessagePack 3.1.4 — Upgraded in Engine and Host to match Common, fixing serialization compatibility.
Archive-based distribution, seaw.exe, mutex/attach/restart directives.
The v0.4 line overhauls distribution (archive-based bootstrapper replaces NuGet tool + lazy staging), adds a windowless CLI binary (seaw.exe), and introduces three new directive families: script mutex, attach protocol, and auto-restart.
- Archive-based install — Per-RID zip archives replace the NuGet tool + Service package + lazy staging model. Archives contain all binaries (sea, seaw, daemon, elevator) pre-built for each platform.
- SeaShell.Bootstrapper — New dotnet tool (
seashellcommand). Embeds per-RID archives and extracts them on install. Subcommands:install,uninstall,start,stop,status.updateis an alias forinstall. seashelldashboard — Runningseashellwith no arguments shows version, platform, daemon/elevator state, and idle time.- SeaShell.Service NuGet package dropped — Archives replace it.
SeaShell.BinariesmarkedIsPackable=false. - install.ps1 / install.sh rewritten — Download archives from GitHub releases,
extract to platform install dir, manage PATH.
SEASHELL_ARCHIVEenv var for offline installs.
- WinExe subsystem binary — Built from SeaShell.Cli with
OutputType=WinExe,AssemblyName=seaw. No console window by default. //sea_consoledirective — Allocates a console for seaw.exe scripts that need stdout. Without it, seaw runs windowless.//sea_windowdirective — Flags scripts as window-mode (used by file association)..cswextension — Window-mode script convention. Associated with seaw.exe.- Event Log errors — When seaw.exe has no console, unhandled errors are written to the Windows Event Log (SeaShell source) instead of lost to stderr.
- ExitDelayIfNeeded — Applied to all non-script code paths in seaw.exe to prevent console flash on errors.
//sea_mutexdirective — Enforces single-instance execution per script. Three scopes:system(default),user,session.- Windows: Named kernel mutex (
Global\SeaShell_{id}for system,SeaShell_{id}_u{user}for user,Local\SeaShell_{id}_s{session}for session). - Linux: File locks (flock) in scope-appropriate directories.
- DirectiveScanner — New lightweight pre-compilation scanner in SeaShell.Invoker. Reads only the first ~20 lines of a script to detect mutex/attach/window/console directives. Resolves mutex blocking instantly without daemon contact.
- Pre-compilation check —
ScriptInvoker.RunAsync()acquires the mutex before contacting the daemon. If blocked and no attach, exits with code 200.
//sea_mutex_attachdirective — When a second instance is blocked by a mutex, it connects to the running instance instead of exiting.- AttachServer — Named pipe listener (
seashell-attach-{identity}) started bySea.Initialize()when//sea_mutex_attachis active. - AttachContext — API provided to scripts via
Sea.Attachedevent:Args,WorkingDirectory,Send(byte[]),Receive(),Close(exitCode). - Wire protocol —
AttachHello(args + CWD),AttachMessage(bidirectional binary payload),AttachClose(exit code). Message types 50-52.
//sea_restartdirective — Script automatically restarts on exit. Distinct from watch-mode reload (no recompilation).- Crash backoff — If a restart cycle completes in under 5 seconds, exponential delay kicks in: 2s, 4s, 8s, 8s... Resets after a successful long run.
- Opt-out —
Sea.Restart = falsewithin a script prevents restart for that run. Sea.RestartCount— Incremented on each restart cycle.
- 4-part version everywhere —
0.4.0.110format:major.minor.patch.build.ToString(4)used in all version display, pipe addressing, and task names. - Single source of truth —
<Version>in Directory.Build.props. Pipeline orchestrator auto-increments the build number before each build. - Side-by-side — 4-part version in pipe addresses means each build gets its own daemon instance. No conflicts during development.
- Daemon hash mismatch —
DaemonServerandDaemonManagernow both use SHA256 of assembly FullNames. Previously used different algorithms, causing unnecessary daemon restarts. - ScriptHost parameter mismatch —
windowModeparameter added to allScriptInvokercall sites. Previously missing from Host path. - seaw.exe console flash —
ExitDelayIfNeededapplied to all non-script exit paths. Previously, error messages vanished instantly. - False ephemeral detection — Parent process detection via
NtQueryInformationProcessreplaces fragileGetConsoleProcessListcounting. The old method miscounted across .NET hosting models. - IncludeResolver merge — v0.3 directive fields (
Restart,MutexScope,MutexAttach,Window,Console) now propagate through//sea_incincludes. - Old task cleanup —
InstallDaemon/InstallElevatorremove stale versioned Task Scheduler tasks before registering new ones.
- Idle time tracking —
PingResponsegainsIdleSecondsandIdleTimeoutSecondsfields. CLI--statusshows idle time and timeout. - Elevator version tracking —
ElevatorHellocarries version string. Daemon stores it,PingResponsereports it,--statusdisplays it. - Handle inheritance guard —
StdHandleInheritGuardtemporarily clearsHANDLE_FLAG_INHERITon stdin/stdout/stderr before spawning daemon. Prevents daemon from inheriting handles that block SSH sessions and pipe readers. - Stdin redirect+close — Daemon process gets all three standard streams redirected and immediately closed, in addition to the handle guard.
- Quiet by default — Daemon lifecycle messages suppressed unless
-V/--verbose/SEA_VERBOSEis set. - Rich
--status— Shows version, uptime, idle time, timeout, active script count, and elevator version.
- Rewritten for archive-based distribution. Produces per-RID zip archives + bootstrapper NuGet package. No Service package.
- seaw.exe — Published on Windows via separate temp dir to avoid csproj output conflicts with sea.exe.
- Archive embedding — Per-RID zips embedded in bootstrapper nupkg. Versioned copies attached to GitHub release.
- Package validation — Checks bootstrapper DLL contains embedded archives (via reflection) and Host contains targets files. Rejects unexpected packages.
- Alpine container fix — Build steps use
shinstead ofbashfor Alpine compatibility (Alpine has no bash). - Attestation + NuGet push preserved from v0.3.
- 87 unit tests (10 new):
IncludeResolverTests(directive field merge),DaemonHashTests(algorithm alignment),MessageRoundtripTests. - New directive tests: Restart, Mutex (all scopes), MutexAttach, Window, Console.
- Pipeline integration tests: mutex user/session scopes, seaw window/console mode, elevated install (elevator + Event Log source), seaw Event Log error.
Invoker refactoring: shared execution engine, package consolidation, security hardening.
The v0.3 line extracts the execution logic into a shared Invoker library. CLI, Host, and ServiceHost are now thin wrappers around the same core — same code paths for compilation, execution, elevation, watch mode, and reload.
- SeaShell.Invoker — New library containing the shared client-side execution engine:
ScriptInvoker(compile, execute, watch, reload, elevate),DaemonLauncher(ensure daemon running),DaemonManager(status, stop, binary staging, version-check),ScheduledTasks(Windows Task Scheduler management),ServiceManifest(installation state tracking). - CLI gutted —
ScriptRunnerreduced from 560 to 35 lines. All execution logic delegated toScriptInvoker.DaemonManagerandScheduledTasksmoved to Invoker. - Host rewritten —
ScriptHostis now a thin wrapper aroundScriptInvokerwithOutputMode.Capture. Dropped direct Engine/Roslyn dependency — compilation happens in the daemon, not in the host process. - ServiceHost merged into Host —
ServiceHostBuilder,ServiceHostWorker, init system detection, and all service installers moved into SeaShell.Host. Same NuGet package, same namespace. Consumers reference one package instead of two. - Package consolidation — 7 public NuGet packages → 3: SeaShell (CLI tool), SeaShell.Host (library + service hosting), SeaShell.Service (daemon/elevator binaries). Internal projects (Common, Engine, Protocol, Invoker, Script) are bundled, not published.
- Service manifest —
seashell.jsonin the data directory tracks installations per version: staged binary paths, hashes, pipe addresses, and registered scheduled tasks. File-locked for concurrent access.ServiceManifest.GetOrStageDaemon()finds daemon binaries from the CLI tool directory or the SeaShell.Service NuGet package, stages them, and records the result. - Side-by-side versioning — Daemon pipe/socket addresses include the 3-part assembly
version (
seashell-{version}-{identity}). Multiple SeaShell versions can run concurrently without conflicts. Each version gets its own daemon, cache, and staged binaries. - Daemon idle timeout — Configurable via
SEASHELL_IDLE_TIMEOUTenvironment variable or--idle-timeoutflag. Supportsms,s,m,hsuffixes. Zero (default) means stay active indefinitely. Useful for CI and ephemeral environments. - Security: service installer argument injection — All five service installers
(Windows Service, systemd, sysvinit, OpenRC, runit) switched from string-based
ProcessStartInfoarguments toArgumentList. Prevents argument injection if service names or paths contain special characters. - Security: Unix socket TOCTOU — Socket and pipe creation on Linux now sets
umask(0077)beforeBind()/new NamedPipeServerStream()and restores after. Closes the brief window where the socket was world-accessible between creation andSetUnixFileMode(). NewPosixUmaskhelper in SeaShell.Common. ExistingSetUnixFileModecalls retained as defense-in-depth. - Pipeline test —
daemon socket permissionstest verifies the daemon socket is created with owner-only permissions (0600) on Linux. Passes on Debian and Alpine.
Alpine Linux (linux-musl-x64) support, pipeline test rework.
- Alpine / musl support — NuGet resolver detects musl-based Linux via
RuntimeInformation.RuntimeIdentifierand/etc/alpine-release, returnslinux-musl-x64RID with fallback chainlinux-musl-x64→linux-x64→linux→any. Daemon manager probesruntimes/linux-musl-x64/for musl apphosts and overwrites glibc variants during staging. Falls back todotnet execif musl apphost is missing. - Pipeline packaging — Injects musl apphosts into
runtimes/linux-musl-x64/in the tool nupkg alongside the existing glibc apphosts intools/net10.0/any/. Conditional on musl artifacts being present. - GitHub Actions CI — Alpine build via
mcr.microsoft.com/dotnet/sdk:10.0-alpinecontainer in the matrix. Release job downloads, injects, and validates musl apphosts. - Pipeline test rework — Three-phase structure: contained tests first, then install/update for both current user and root/SYSTEM, then smoke tests. No rollback — develop forward. Clears all caches (compilation, daemon staging, elevator staging) and unregisters stale Task Scheduler tasks before install.
Daemon staging fix, .NET 10 dependency cleanup, cache hash includes direct NuGet versions.
- Recursive daemon staging —
StageBinary()now copies subdirectories (includingruntimes/) when staging daemon and elevator binaries. Previously only top-level files were copied, causingSystem.Diagnostics.EventLog.Messages.dll(inruntimes/win/lib/) to be missing at runtime. Fixes daemon startup failure on machines where the NuGet cache doesn't have the package (e.g., installed as a dotnet tool, not built from source).ComputeDirHash()updated to include DLLs from subdirectories. - System.Diagnostics.EventLog upgraded to 10.0.5 — Overrides the transitive 8.0.0
(from Serilog.Sinks.EventLog 4.0.0) via Directory.Build.props. The 8.0.0 package only
ships
lib/net8.0/andruntimes/win/lib/net8.0/assets; 10.0.5 has propernet10.0assets. Also upgrades ServiceHost from the preview version to stable. - System.IO.Pipes.AccessControl removed — The 5.0.0 NuGet package (only
lib/net5.0/assets) is unnecessary on .NET 10 wherePipeSecurity,PipeAccessRule, andNamedPipeServerStreamAclare inbox in the shared framework. Removed from Protocol.csproj along with theNU1510warning suppression. - Cache hash includes direct NuGet versions —
CompilationCache.ComputeHash()now includes resolved versions of direct//sea_nugetpackage references. For explicit versions this is zero-cost (already in source text). For versionless directives, a singleDirectory.GetDirectories()call resolves the latest version from the NuGet cache (~1-2ms). Transitive dependencies are not included (immutable by convention). This ensures versionless//sea_nuget Foodirectives pick up new package versions without requiring a source change.
Self-contained compilation output. The output directory is now standalone like dotnet publish
— all DLLs copied at compile time, no runtime dependency on the NuGet cache. Re-runs skip
NuGet resolution entirely.
- Self-contained output — All NuGet package DLLs (managed and native) are copied to the
output dir at compile time. All
.deps.jsonentries use type:"project" (resolved from app base dir). NoadditionalProbingPathsin.runtimeconfig.json. The NuGet cache is only needed during compilation, never at runtime. Eliminates file lock contention, cross-user cache path issues, and stale-cache runtime failures under service accounts. - Cache hash simplified — Hash is now source content + engine fingerprint only. NuGet package versions removed (packages are immutable by convention). NuGet resolution skipped entirely on cache hits — re-runs are instant (~1ms include resolution + hash check).
- Compilation pipeline reordered — NuGet resolution moved after the cache check. On cache hit: resolve includes → hash → return cached artifacts. NuGet resolution (10-200ms) only runs on cache miss (first compilation or source change).
- Snippet caching —
ScriptHost.RunSnippetAsyncuses content-based filenames instead of random GUIDs. Identical code maps to the same cache key. Cache hit = zero disk I/O (no temp file creation, no NuGet resolution, no compilation). - StartupHook namespace fix —
StartupHookmoved fromSeaShellnamespace to root namespace.DOTNET_STARTUP_HOOKSrequiresAssembly.GetType("StartupHook")which only matches types without a namespace. Fixes Sea context injection for pre-compiled binaries run viasea myapp.dll. - Binary deps.json merge —
CompileBinary()now merges SeaShell's bundled entries into companion.deps.jsonand adds probing paths to companion.runtimeconfig.json, instead of copying them verbatim. Pre-compiled binaries with their own NuGet dependencies get correct SeaShell runtime resolution alongside their own packages. - Test suite expanded — 14 pipeline tests on both Windows and Linux (was 10):
host-in-host-nuget: nested ScriptHost where inner snippet uses NuGetengine-dir-nuget: ScriptHost viadotnet run(NuGet cache layout regression guard)binary-deps: pre-compiled binary with companion deps.json + Sea context via startup hookservice-cwd: ServiceHost with//sea_nuget seashell.hostunder SYSTEM/rootservice-identity: NuGet cache isolation verified on service account switch
- Unit tests expanded — DepsJsonWriter tests for version skew, NuGet overlap with bundled DLLs, empty engine dir fallback. ArtifactWriter tests updated for self-contained output.
Bundled DLL resolution fix, engine dir probing, unit test suite.
- Engine dir in probing paths —
ArtifactWriter.WriteRuntimeConfignow includes the engine binary directory inadditionalProbingPaths. Script subprocesses can find bundled DLLs (MessagePack, SeaShell.Ipc, SeaShell.Script) via the host's directory as a fallback when the copy to the output dir is skipped or fails. Fixes assembly resolution failures in host-embedded scenarios (e.g., service host bridge scripts). - Dynamic bundled DLL versions —
DepsJsonWriterreads actual assembly versions from the bundled DLLs viaAssemblyName.GetAssemblyName()instead of hardcoding"1.0.0". The old hardcoded versions caused the .NET host to fail resolving MessagePack from the NuGet cache. - DirectiveParser unit tests — 42 tests covering whitespace, comments containing directives (the CS-Script bug), scanning boundaries, NuGet variants, VB support.
- DepsJsonWriter + ArtifactWriter unit tests — Verify bundled entries use correct versions, runtimeconfig includes engine dir and NuGet cache in probing paths.
- Host-resolution integration test — End-to-end test in the pipeline: ScriptHost compiles and runs a script that verifies MessagePack.dll and SeaShell.Script.dll resolve correctly. Catches the exact class of bug that hit host-embedded consumers.
- Build attestation — CI workflow attests all
.nupkgfiles withactions/attest-build-provenance(Sigstore + SLSA provenance). Consumers verify withgh attestation verify <file> --repo PLN/SeaShell. - Conditional NuGet push — Prerelease tags (e.g.,
v0.1.12-rc1) run the full CI pipeline but skip the nuget.org push. Clean version tags push as before. - InternalsVisibleTo — Engine exposes internals to the EngineTest project.
Engine self-contained DLL resolution, Host packaging fixes, local CI/CD pipeline.
- Self-contained DLL resolution — Engine locates bundled DLLs from its own assembly
directory (
typeof(ScriptCompiler).Assembly.Location), correct for both daemon and NuGet-hosted ScriptHost scenarios. Falls back toAppContext.BaseDirectory. - Host NuGet packaging —
build/andbuildTransitive/directories in the Host package carry.targetsfiles that inject SeaShell.Script as a compile-time reference and copy MessagePack/Ipc DLLs to consumers' output directories. - SeaShell.Script removed as standalone NuGet — Script is now bundled as a binary
inside Engine, Host, and the CLI tool packages. No standalone
SeaShell.ScriptNuGet package. This eliminates version conflicts when multiple packages reference different Script versions. - Host-in-Host regression test — Pipeline test that runs ScriptHost from inside a Host consumer. Catches CS1704 (duplicate assembly) regressions.
- NuGet transitive dedup — Engine deduplicates compile-time references when a script's NuGet dependencies transitively include assemblies already bundled by the Engine.
- Local CI/CD pipeline — Build, package, and test on both Windows and Linux build hosts via SSH. Shared storage for cross-platform artifact exchange. Convention-based directory layout with timestamp correlation IDs.
Daemon hash fix.
- Daemon hash mismatch on Task Scheduler — Fixed: Task Scheduler launches used the tool store path directly, bypassing the staged daemon directory. The CLI's hash check always saw a mismatch and restarted the daemon on every script run. Now consistently launches from the staged directory.
Binary running, script-initiated reload, cross-platform service hosting, and daemon staging.
- Binary running —
sea myapp.dllandhost.RunAsync("myapp.dll")run pre-compiled assemblies with full Sea context.ScriptCompiler.Compile()detects.dll/.exe/extensionless binaries and stages them with companion files — both CLI and Host get this for free. Three .exe tiers: Tier 1 (apphost → redirect to .dll), Tier 2 (framework-dependent single-file), Tier 3 (self-contained single-file). Companion.sea.jsonfile enables directives for binaries ({"watch": true}). ASP.NET Core auto-detected from PE assembly references. Sea.RequestReload()— Scripts can programmatically trigger their own reload with optional cache clearing. Works in both direct and watch mode. In direct mode the CLI opens a new daemon connection; in watch mode it sendsRecompileRequestover the held connection. The Host handles it internally (it owns the compiler). New IPC messages:ScriptReloadRequest(tag 8),RecompileRequest(tag 30).RunRequestgainedClearCachefield.CompilationCache.ClearScript()— Deletes all cache directories for a script, used byRequestReload(clearCache: true).Sea.IsWatchMode— Public property set fromScriptInit.Watch.StartupHook—DOTNET_STARTUP_HOOKScontract class in SeaShell.Script for injecting Sea context into .dll binaries that don't reference SeaShell.Script.AssemblyInspector— PE metadata inspection viaSystem.Reflection.Metadata. Checks for SeaShell.Script and ASP.NET Core references without loading the assembly.ScriptHostreload loop —ExecuteAsyncnow handlesScriptReloadRequestinternally with a restart loop. Watch mode (//sea_watch) startsScriptWatcherembedded in the Host — no daemon needed.ScriptConnection.StopAsync()sendsScriptStopfor graceful shutdown.SeaShell.ServiceHost— New NuGet package. Cross-platform service hosting with a 6-line consumer API (ServiceHostBuilder). Auto-detects init system (Windows Service, systemd, runit, OpenRC, sysvinit).install/uninstall/start/stop/statusmanagement commands built into the binary.ServiceHostWorkerruns scripts with crash recovery and optional NuGet update loop for zero-locking automatic updates. Embedded init script templates.- Graceful service shutdown —
RunOneInstanceAsyncsendsScriptStopthrough IPC on cancellation token, waits for graceful exit, kills after timeout. No orphaned child processes. - systemd
KillMode=process— Only the main PID receives SIGTERM; child script process gets gracefulScriptStopvia IPC instead of direct signal kill. - CI service smoke test — Full install/start/reload/stop/uninstall cycle on both Windows and Linux runners. Verifies PID change (reload worked) and no orphaned processes.
- Linux binary support — Extensionless ELF binaries detected and handled as direct executables (same as Tier 2/3 .exe on Windows).
HotSwapNotifyextended — CarriesStartupHookPathandDirectExefields so watch-mode restarts preserve binary execution mode.SeaShellPaths— New static class centralizing all data directory paths. Per-user%LOCALAPPDATA%\seashell\(Windows) or~/.local/share/seashell/(Linux). System accounts (SYSTEM/root) use%ProgramData%\seashell\or/var/lib/seashell/. Override withSEASHELL_DATAenv var. Compilation cache moved from%TEMP%to persistent AppData.- Daemon staging —
DaemonManager.StartDaemon()copies daemon binaries to{DataDir}/daemon/{hash}/before launching. Eliminates DLL lock conflicts:dotnet buildsucceeds while daemon is running. Same for elevator via{DataDir}/elevator/{hash}/.--install-daemon/--install-elevatorregister staged paths with Task Scheduler. - Daemon version-check —
PingResponsegainsPidandDaemonHashfields. On script run, CLI compares running daemon's hash with staged hash. Mismatch triggers automatic restart. Last-resort PID kill if IPC stop fails. .runtimeconfig.dev.jsongeneration — Staged binaries get a dev config withadditionalProbingPathspointing to the NuGet cache, so package dependencies (EventLog, Serilog sinks, etc.) resolve correctly from the staging directory.
Daemon lifecycle, cross-platform packaging, and one-liner installers.
--stopvia IPC —sea --stopnow uses IPC first (works for any running daemon, including on Linux), then ends Task Scheduler tasks if registered. Reports actual state — no misleading output when nothing is running. Output aligned with--statusformat.- Task preference —
StartDaemonprefers a registered Task Scheduler task overProcess.Start. Falls back to spawning a process if no task is registered. - Elevator auto-start — When a
//sea_elevatescript fails because the elevator isn't connected, the CLI starts the elevator task and the daemon waits internally for the elevator'sElevatorHello(AwaitElevatorMsonSpawnRequest). One round trip, no CLI polling. - Independent binary search —
--install-daemononly searches for the daemon binary,--install-elevatoronly for the elevator. No more confusing cross-errors. - Dotnet tool mode —
FindBinarydetects.dll(dotnet tool store) and registers tasks withdotnet execas fallback.BuildTaskXmlsupports<Arguments>element. - Cross-platform nupkg — CI packs on Windows (gets
.exeWinExe apphosts natively), then injects Linux ELF apphosts into the nupkg. Both platforms get native executables — daemon and elevator use WinExe subsystem on Windows (no console window). - CI lifecycle tests —
--status,--stop, and idempotent stop tested on both Windows and Linux runners. install.ps1— One-liner Windows installer: checks .NET 10, installs/updates tool, stops running instances, registers daemon task, optionally registers elevator via gsudo, associates.cs. Usage:iex (irm https://raw.githubusercontent.com/PLN/SeaShell/main/install.ps1)install.sh— One-liner Linux installer: checks .NET 10, stops running instances, installs/updates tool. Usage:curl -fsSL https://raw.githubusercontent.com/PLN/SeaShell/main/install.sh | sh
Binary IPC and Host↔Script messaging.
- Binary wire format — Replaced JSON Envelope framing with binary MessagePack
serialization. Wire format:
[4-byte LE length][1-byte type tag][MessagePack payload].MessageTypebyte enum replaces string-based type discrimination. - MessagePack — Contractless resolver (plain C# records, no attributes). Zero-copy
byte[]payloads, no Base64 encoding for binary data. ~2-10x faster than JSON. - Host↔Script messaging — New
HostMessage/ScriptMessagerecords with binary payload and optional topic.Sea.MessageReceivedevent +Sea.SendMessage/SendMessageAsyncon the script side.ScriptHost.ScriptConnectionwith event-based receive +SendAsyncon the Host side. CLI silently ignores script messages. - TransportStream simplified — Now a thin wrapper exposing
MessageChannel. RawSendAsync(byte[])/ReceiveAsync()replaced with typed binary messaging. Envelope.csdeleted — All JSON serialization removed from IPC layer.- GitHub Actions CI — Build + smoke tests on Windows and Linux. Tag-triggered
NuGet publish (
v*tags push to nuget.org).
Unified IPC and branding.
- SeaShell.Ipc — New shared IPC project.
MessageChanneloverSystem.IO.Pipelineswith length-prefixedEnvelopeframing. Replaces ad-hoc env vars and sidecar files with a single bidirectional named pipe between launcher and script. - Script pipe — Script creates the pipe server, CLI/Host connects as client.
Full lifecycle control:
ScriptInit→ScriptReload/ScriptStop→ScriptState→ScriptExit. - Elevated console — Elevated scripts call
FreeConsole+AttachConsoleto share the CLI's console window. Console title changes, colored output, and interactive input all work transparently.WindowStyle.Hiddeneliminates the brief console flash. - Per-user task names — Scheduled tasks include the username (
SeaShell Daemon (user)) to avoid conflicts on multi-user machines. - Pipe security — Per-user ACL on Windows (current user SID, FullControl regardless of integrity level). Owner-only socket permissions on Linux (0600).
_SeaShellBootmodule initializer — Injected into every compiled script (C# and VB.NET) to guaranteeSea.Initialize()runs even if the script never referencesSea.*.{~}branding — SVG/ICO icon embedded in all binaries.--helpand--versionshow the{~} SeaShellheader with version and copyright.--associate/--unassociate— Per-user file type registration via HKCU (no elevation needed). Double-click a.csfile to run it with SeaShell.- Package hygiene — Protocol, Daemon, and Elevator marked
IsPackable=false. Package READMEs added for all public packages.
Initial release.
- Roslyn-based C# and VB.NET scripting engine
- Persistent daemon with warm Roslyn instance
- NuGet resolution with transitive dependencies and runtime-specific DLLs
.deps.jsongeneration for correct assembly probing- Hot-swap (
//sea_watch) with state handoff between instances - REPL with stateful sessions
- Elevation via Task Scheduler worker (
//sea_elevate) - Embeddable host library (
SeaShell.Host) - CS-Script directive compatibility (
//css_inc,//css_nuget, etc.) - Cross-platform: named pipes on Windows, Unix domain sockets on Linux
- Background NuGet update checker (8-hour cycle)
- Mixed declaration order (classes and top-level statements in any order)
- Interactive exit delay for ephemeral consoles