Skip to content

Latest commit

 

History

History
851 lines (755 loc) · 53.3 KB

HISTORY.md

File metadata and controls

851 lines (755 loc) · 53.3 KB

History

v0.6.2 (2026-10-07)

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_incdir directories 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 under runtimes/unix/lib/ (e.g. Microsoft.Data.SqlClient) fell back to the lib/ stub that throws PlatformNotSupportedException. 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 versioned libfoo.so.N natives, 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-build on a fresh checkout skipped restore and silently ran nothing, and the Windows seaw tests 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 RunAsync waited 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 new ScriptResult.Cancelled set. 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_restart works in watch mode. A //sea_watch script 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.

v0.6.1 (2026-07-08)

NuGet dependency resolution fix for DacFx and runtime-specific assets.

  • Direct //sea_nuget versions now win over transitives. Multiple package directives are resolved as one graph, preserving a direct pin such as System.Runtime.Caching 8.0.1 even when a dependency requests an older floor.
  • Runtime asset selection avoids netstandard shims when real net6+ assets exist. RID-specific managed assets and generic lib/net6.0+ assets are preferred over compatibility throw-shims for a net10 host.
  • Unsupported NuGet directive diagnostics. #r "nuget: ..." and #:package now report a SeaShell-specific message pointing users to //sea_nuget <Package> [version].
  • Stale installers no longer downgrade scheduled runtimes. When a target's current junction 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 to seashell status --system, whose output includes the SYSTEM current junction 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, …) threw DllNotFoundException at first use: native files were staged and declared at their package-relative subpath while every library is type:"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).

v0.6.0 (2026-06-11)

SID-based identity, direct elevated SYSTEM install, install --all single-elevation setup.

  • Identity is explicit and SID-based. New SeaShellIdentity type: 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 of Environment.UserName — canonical, locale-independent, and stable under LocalSystem. The key property is that a TARGET identity is computable from a different security context: SeaShellIdentity.LocalSystem describes the SYSTEM install (well-known S-1-5-18, systemprofile data 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, and LegacyToken keeps installers able to recognize and stop old daemons during migration.
  • --system install runs directly in the elevated bootstrapper. The entire transient-task machinery is gone: SystemOps, SystemOpRunner, the cmd.exe wrapper templates, nupkg tool-store probing, ProgramData staging, and the 300s sentinel polling. Registering an S-1-5-18 boot-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 under systemprofile\.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-sid guard 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\Administrators ReadWrite. An elevated installer can ping/stop daemons of other identities gracefully — most importantly SYSTEM's during --system upgrades. 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 --system install contend on one lock.
  • Stop flow hardened. StopAsync default 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. DaemonLauncher compared daemon versions with ordinal string compare in two spots (the v0.4.4 bug class); now Version.Parse.
  • Junction self-heal. IsJunction/ReadTarget/CreateOrUpdate handle orphaned links: a dangling current junction reads as "no current version" and is deleted and recreated on install instead of being trusted.
  • seashell status --system reads 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.

v0.5.0 (2026-04-18)

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 own bin/, cache/, and marker files. A current/ 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.pid are 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 --system installs under SYSTEM's own user profile. Previously --system routed to %ProgramData%\seashell\ (a machine-wide location) and baked the invoking elevated user's identity into task XML + marker content. With Environment.UserName returning {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: --system registers a transient one-shot scheduled task with principal S-1-5-18, which runs a plain seashell install as 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 a BootTrigger so Windows services running as SYSTEM can find a warm daemon at boot.
  • SeaShell.Installer assembly. 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 on SeaShell.Common + SeaShell.Protocol — no Roslyn or scripting surface. The CLI's --install-daemon, --install-elevator, --uninstall-daemon, --uninstall-elevator, --associate, and --unassociate flags are removed; install lifecycle lives entirely in the seashell command now.
  • Pre-0.5.0 layout cleanup on upgrade. seashell install detects 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 locked bin/ is moved aside as bin.remove-{timestamp}/ and cleaned up on the next install pass once the lock releases.
  • Post-install verification block. Every seashell install ends with a verification pass: marker files exist and point at real binaries, current junction resolves to the version dir, sea --version reports 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 install and seashell uninstall acquire 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.DaemonHash and the ComputeSelfHash / ComputeDirHash pair 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.
  • ProgramData no longer used. SeaShellPaths always resolves to LocalAppData on Windows. SYSTEM's LocalAppData lives 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.ps1 and install.sh curl/iex one-liners are gone — dotnet tool install -g SeaShell is now the only install path. Offline / archive-based installs use --add-source <dir> on the same command.

v0.4.7.213 (2026-04-11)

Runtime daemon discovery, install cleanup, locked-file install robustness.

  • daemon.address removed from manifest — runtime enumeration replaces install-time prediction. The daemon.address field in seashell.json was written by the installer using its own Environment.UserName, which diverges from the daemon's runtime identity when the two run under different principals — notably on .NET 10 under LocalSystem, where Environment.UserName returns {MachineName}$, not "SYSTEM". The field is deleted from ComponentEntry (System.Text.Json's default UnmappedMemberHandling=Skip silently drops it on read, so existing manifests migrate automatically) and replaced with a new TransportEndpoint.EnumerateDaemonEndpoints(identity) helper that enumerates live named pipes on Windows (\\.\pipe\seashell-*) or socket files on Linux ($XDG_RUNTIME_DIR/seashell-*.sock with Path.GetTempPath fallback). ServiceManifest.GetCompatibleDaemonAddresses now 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 IsSocketResponding on Windows returning false unconditionally, 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 --system scheduled-task registration and the matching identity capture in ScheduledTasks.cs, which share the same identity-mismatch root cause and need to be designed cohesively.
  • seashell status --system diagnostic. New command that enumerates pipes under \\.\pipe\seashell-*, filters out the current principal's own pipes, and probes each remaining pipe with NamedPipeClientStream.Connect. Classifies each as reachable / 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 .old file was still held open by a running process — causing the second install to crash with IOException: Cannot create a file when that file already exists. The suffix now includes DateTime.UtcNow.Ticks so 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.

v0.4.6.211 (2026-04-11)

Fix named-pipe accept-loop race on Windows.

  • Listener pool — TransportServer on Windows used to create exactly one NamedPipeServerStream instance per AcceptAsync call, leaving a window with zero listening instances between accept-loop iterations. Clients probing in that window hit ERROR_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.
  • PipeSecurity lifetime — Built once in the TransportServer constructor and shared across all pool instances. Mismatched security across instances of the same pipe name would otherwise throw from Create.
  • AcceptAsync / accept loop — AcceptAsync gains an optional maxWait parameter that returns null on 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 linked CancellationTokenSource plus an OperationCanceledException round-trip.

v0.4.5.209 (2026-04-09)

Install rename-on-write fallback on Linux.

  • Bootstrapper — The locked-file fallback (rename .old, extract new) was guarded by isWindows. On Linux, File.Move works on a running binary (inode stays) and File.Delete of the .old succeeds, so the same strategy applies. Removed the guard.

v0.4.4.207 (2026-04-08)

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 first sea invocation. 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 use Version.Parse() instead of lexicographic string comparison. Fixes incorrect version ordering when segment values cross digit boundaries (e.g., v49 > v50 under string sort).

v0.4.3.204 (2026-04-07)

Per-script apphosts, console title, status improvements.

  • Per-script apphost — AppHostWriter generates a native executable per script instead of using dotnet exec. Scripts launch instantly as native processes with correct process names in task manager.
  • Console title — sea sets the console title to {scriptName} — SeaShell v{version} while a script is running.
  • seashell status improvements — Shows installed versions from the service manifest with daemon/elevator health status. Detects and reports broken staging directories.

v0.4.2.194 (2026-04-06)

Compatible daemon discovery.

  • DaemonLauncher version fallback — When the exact-version daemon isn't running, DaemonLauncher queries 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.

v0.4.1.190 (2026-04-04)

Gitea Actions CI, bootstrapper improvements, bug fixes.

CI/CD

  • 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).

Bug fixes

  • Bootstrapper DataDir for SYSTEM — GetDataDir() detects SYSTEM/LocalService/ NetworkService SIDs and uses %ProgramData%\seashell instead 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 of GetConsoleProcessList() for console detection. Compilation errors logged to Event Log when no console is available.
  • windowMode passthrough — windowMode parameter propagated through all ScriptInit paths. Previously missing from some code paths, causing incorrect console behavior.
  • OpenRC/sysvinit service start — Init scripts stripped of CRLF line endings. /sbin added to PATH before invoking OpenRC/sysvinit commands on Alpine.
  • MessagePack 3.1.4 — Upgraded in Engine and Host to match Common, fixing serialization compatibility.

v0.4.0.110 (2026-03-31)

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.

Distribution

  • 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 (seashell command). Embeds per-RID archives and extracts them on install. Subcommands: install, uninstall, start, stop, status. update is an alias for install.
  • seashell dashboard — Running seashell with no arguments shows version, platform, daemon/elevator state, and idle time.
  • SeaShell.Service NuGet package dropped — Archives replace it. SeaShell.Binaries marked IsPackable=false.
  • install.ps1 / install.sh rewritten — Download archives from GitHub releases, extract to platform install dir, manage PATH. SEASHELL_ARCHIVE env var for offline installs.

seaw.exe

  • WinExe subsystem binary — Built from SeaShell.Cli with OutputType=WinExe, AssemblyName=seaw. No console window by default.
  • //sea_console directive — Allocates a console for seaw.exe scripts that need stdout. Without it, seaw runs windowless.
  • //sea_window directive — Flags scripts as window-mode (used by file association).
  • .csw extension — 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.

Script mutex

  • //sea_mutex directive — 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.

Attach protocol

  • //sea_mutex_attach directive — 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 by Sea.Initialize() when //sea_mutex_attach is active.
  • AttachContext — API provided to scripts via Sea.Attached event: Args, WorkingDirectory, Send(byte[]), Receive(), Close(exitCode).
  • Wire protocol — AttachHello (args + CWD), AttachMessage (bidirectional binary payload), AttachClose (exit code). Message types 50-52.

Restart

  • //sea_restart directive — 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 = false within a script prevents restart for that run.
  • Sea.RestartCount — Incremented on each restart cycle.

Version scheme

  • 4-part version everywhere — 0.4.0.110 format: 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.

Bug fixes

  • Daemon hash mismatch — DaemonServer and DaemonManager now both use SHA256 of assembly FullNames. Previously used different algorithms, causing unnecessary daemon restarts.
  • ScriptHost parameter mismatch — windowMode parameter added to all ScriptInvoker call sites. Previously missing from Host path.
  • seaw.exe console flash — ExitDelayIfNeeded applied to all non-script exit paths. Previously, error messages vanished instantly.
  • False ephemeral detection — Parent process detection via NtQueryInformationProcess replaces fragile GetConsoleProcessList counting. The old method miscounted across .NET hosting models.
  • IncludeResolver merge — v0.3 directive fields (Restart, MutexScope, MutexAttach, Window, Console) now propagate through //sea_inc includes.
  • Old task cleanup — InstallDaemon/InstallElevator remove stale versioned Task Scheduler tasks before registering new ones.

Daemon improvements

  • Idle time tracking — PingResponse gains IdleSeconds and IdleTimeoutSeconds fields. CLI --status shows idle time and timeout.
  • Elevator version tracking — ElevatorHello carries version string. Daemon stores it, PingResponse reports it, --status displays it.
  • Handle inheritance guard — StdHandleInheritGuard temporarily clears HANDLE_FLAG_INHERIT on 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_VERBOSE is set.
  • Rich --status — Shows version, uptime, idle time, timeout, active script count, and elevator version.

CI workflow

  • 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 sh instead of bash for Alpine compatibility (Alpine has no bash).
  • Attestation + NuGet push preserved from v0.3.

Tests

  • 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.

v0.3.17 (2026-03-29)

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 — ScriptRunner reduced from 560 to 35 lines. All execution logic delegated to ScriptInvoker. DaemonManager and ScheduledTasks moved to Invoker.
  • Host rewritten — ScriptHost is now a thin wrapper around ScriptInvoker with OutputMode.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.json in 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_TIMEOUT environment variable or --idle-timeout flag. Supports ms, s, m, h suffixes. 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 ProcessStartInfo arguments to ArgumentList. 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) before Bind()/new NamedPipeServerStream() and restores after. Closes the brief window where the socket was world-accessible between creation and SetUnixFileMode(). New PosixUmask helper in SeaShell.Common. Existing SetUnixFileMode calls retained as defense-in-depth.
  • Pipeline test — daemon socket permissions test verifies the daemon socket is created with owner-only permissions (0600) on Linux. Passes on Debian and Alpine.

v0.2.16 (2026-03-28)

Alpine Linux (linux-musl-x64) support, pipeline test rework.

  • Alpine / musl support — NuGet resolver detects musl-based Linux via RuntimeInformation.RuntimeIdentifier and /etc/alpine-release, returns linux-musl-x64 RID with fallback chain linux-musl-x64 → linux-x64 → linux → any. Daemon manager probes runtimes/linux-musl-x64/ for musl apphosts and overwrites glibc variants during staging. Falls back to dotnet exec if musl apphost is missing.
  • Pipeline packaging — Injects musl apphosts into runtimes/linux-musl-x64/ in the tool nupkg alongside the existing glibc apphosts in tools/net10.0/any/. Conditional on musl artifacts being present.
  • GitHub Actions CI — Alpine build via mcr.microsoft.com/dotnet/sdk:10.0-alpine container 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.

v0.2.15 (2026-03-27)

Daemon staging fix, .NET 10 dependency cleanup, cache hash includes direct NuGet versions.

  • Recursive daemon staging — StageBinary() now copies subdirectories (including runtimes/) when staging daemon and elevator binaries. Previously only top-level files were copied, causing System.Diagnostics.EventLog.Messages.dll (in runtimes/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/ and runtimes/win/lib/net8.0/ assets; 10.0.5 has proper net10.0 assets. 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 where PipeSecurity, PipeAccessRule, and NamedPipeServerStreamAcl are inbox in the shared framework. Removed from Protocol.csproj along with the NU1510 warning suppression.
  • Cache hash includes direct NuGet versions — CompilationCache.ComputeHash() now includes resolved versions of direct //sea_nuget package references. For explicit versions this is zero-cost (already in source text). For versionless directives, a single Directory.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 Foo directives pick up new package versions without requiring a source change.

v0.2.14 (2026-03-25)

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.json entries use type:"project" (resolved from app base dir). No additionalProbingPaths in .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.RunSnippetAsync uses 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 — StartupHook moved from SeaShell namespace to root namespace. DOTNET_STARTUP_HOOKS requires Assembly.GetType("StartupHook") which only matches types without a namespace. Fixes Sea context injection for pre-compiled binaries run via sea myapp.dll.
  • Binary deps.json merge — CompileBinary() now merges SeaShell's bundled entries into companion .deps.json and 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 NuGet
    • engine-dir-nuget: ScriptHost via dotnet run (NuGet cache layout regression guard)
    • binary-deps: pre-compiled binary with companion deps.json + Sea context via startup hook
    • service-cwd: ServiceHost with //sea_nuget seashell.host under SYSTEM/root
    • service-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.

v0.1.12 (2026-03-24)

Bundled DLL resolution fix, engine dir probing, unit test suite.

  • Engine dir in probing paths — ArtifactWriter.WriteRuntimeConfig now includes the engine binary directory in additionalProbingPaths. 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 — DepsJsonWriter reads actual assembly versions from the bundled DLLs via AssemblyName.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 .nupkg files with actions/attest-build-provenance (Sigstore + SLSA provenance). Consumers verify with gh 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.

v0.1.10 (2026-03-23)

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 to AppContext.BaseDirectory.
  • Host NuGet packaging — build/ and buildTransitive/ directories in the Host package carry .targets files 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.Script NuGet 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.

v0.1.7 (2026-03-22)

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.

v0.1.6 (2026-03-22)

Binary running, script-initiated reload, cross-platform service hosting, and daemon staging.

  • Binary running — sea myapp.dll and host.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.json file 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 sends RecompileRequest over the held connection. The Host handles it internally (it owns the compiler). New IPC messages: ScriptReloadRequest (tag 8), RecompileRequest (tag 30). RunRequest gained ClearCache field.
  • CompilationCache.ClearScript() — Deletes all cache directories for a script, used by RequestReload(clearCache: true).
  • Sea.IsWatchMode — Public property set from ScriptInit.Watch.
  • StartupHook — DOTNET_STARTUP_HOOKS contract class in SeaShell.Script for injecting Sea context into .dll binaries that don't reference SeaShell.Script.
  • AssemblyInspector — PE metadata inspection via System.Reflection.Metadata. Checks for SeaShell.Script and ASP.NET Core references without loading the assembly.
  • ScriptHost reload loop — ExecuteAsync now handles ScriptReloadRequest internally with a restart loop. Watch mode (//sea_watch) starts ScriptWatcher embedded in the Host — no daemon needed. ScriptConnection.StopAsync() sends ScriptStop for 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/status management commands built into the binary. ServiceHostWorker runs scripts with crash recovery and optional NuGet update loop for zero-locking automatic updates. Embedded init script templates.
  • Graceful service shutdown — RunOneInstanceAsync sends ScriptStop through 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 graceful ScriptStop via 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).
  • HotSwapNotify extended — Carries StartupHookPath and DirectExe fields 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 with SEASHELL_DATA env 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 build succeeds while daemon is running. Same for elevator via {DataDir}/elevator/{hash}/. --install-daemon/--install-elevator register staged paths with Task Scheduler.
  • Daemon version-check — PingResponse gains Pid and DaemonHash fields. 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.json generation — Staged binaries get a dev config with additionalProbingPaths pointing to the NuGet cache, so package dependencies (EventLog, Serilog sinks, etc.) resolve correctly from the staging directory.

v0.1.5 (2026-03-22)

Daemon lifecycle, cross-platform packaging, and one-liner installers.

  • --stop via IPC — sea --stop now 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 --status format.
  • Task preference — StartDaemon prefers a registered Task Scheduler task over Process.Start. Falls back to spawning a process if no task is registered.
  • Elevator auto-start — When a //sea_elevate script fails because the elevator isn't connected, the CLI starts the elevator task and the daemon waits internally for the elevator's ElevatorHello (AwaitElevatorMs on SpawnRequest). One round trip, no CLI polling.
  • Independent binary search — --install-daemon only searches for the daemon binary, --install-elevator only for the elevator. No more confusing cross-errors.
  • Dotnet tool mode — FindBinary detects .dll (dotnet tool store) and registers tasks with dotnet exec as fallback. BuildTaskXml supports <Arguments> element.
  • Cross-platform nupkg — CI packs on Windows (gets .exe WinExe 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

v0.1.2 (2026-03-20)

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]. MessageType byte 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/ScriptMessage records with binary payload and optional topic. Sea.MessageReceived event + Sea.SendMessage/SendMessageAsync on the script side. ScriptHost.ScriptConnection with event-based receive + SendAsync on the Host side. CLI silently ignores script messages.
  • TransportStream simplified — Now a thin wrapper exposing MessageChannel. Raw SendAsync(byte[])/ReceiveAsync() replaced with typed binary messaging.
  • Envelope.cs deleted — 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).

v0.1.1 (2026-03-20)

Unified IPC and branding.

  • SeaShell.Ipc — New shared IPC project. MessageChannel over System.IO.Pipelines with length-prefixed Envelope framing. 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 + AttachConsole to share the CLI's console window. Console title changes, colored output, and interactive input all work transparently. WindowStyle.Hidden eliminates 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).
  • _SeaShellBoot module initializer — Injected into every compiled script (C# and VB.NET) to guarantee Sea.Initialize() runs even if the script never references Sea.*.
  • {~} branding — SVG/ICO icon embedded in all binaries. --help and --version show the {~} SeaShell header with version and copyright.
  • --associate/--unassociate — Per-user file type registration via HKCU (no elevation needed). Double-click a .cs file to run it with SeaShell.
  • Package hygiene — Protocol, Daemon, and Elevator marked IsPackable=false. Package READMEs added for all public packages.

v0.1.0 (2026-03-19)

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.json generation 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