File: AppHost.cs
Web Access
Project: src\playground\Terminals\Terminals.AppHost\Terminals.AppHost.csproj (Terminals.AppHost)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
// WithTerminal() is currently experimental (ASPIRETERMINAL001). Suppressed here
// because this playground project intentionally exercises the experimental API.
#pragma warning disable ASPIRETERMINAL001
 
using Terminals.AppHost;
 
var builder = DistributedApplication.CreateBuilder(args);
 
builder.AddPostgres("postgres").WithRepl();
builder.AddRedis("redis").WithRepl();
builder.AddValkey("valkey").WithRepl();
builder.AddMongoDB("mongo").WithRepl();
builder.AddMySql("mysql").WithRepl();
builder.AddSqlServer("sqlserver").WithRepl();
 
// A multi-replica project that calls `WithTerminal()` so each replica gets its
// own pseudo-terminal and the dashboard can attach to any of them via
// `/api/terminal?resource=repl&replica=<i>`. The replica index is forwarded as
// an environment variable so the REPL can stamp it on its banner.
builder.AddProject<Projects.Terminals_Repl>("repl")
    .WithReplicas(2)
    .WithEnvironment("ASPIRE_RESOURCE_NAME", "repl")
    .WithTerminal(options =>
    {
        options.Columns = 120;
        options.Rows = 32;
        options.ShowTerminalHost = true;
    })
    // Opens a shell owned by the AppHost rather than orchestrated by Aspire. It has nothing to do with `repl`;
    // commands just need a host resource to hang off.
    .WithAppHostShellCommand()
    // Drives an interactive console program from AppHost code: the terminal is shown in a dialog, but the guesses
    // are typed by the AppHost, which reads each reply back off the screen and bisects until it wins.
    .WithNumberGuessCommand()
    // Automates a terminal the AppHost does NOT own: it joins `shell`'s existing terminal as an extra viewer and
    // types into it, which is what shelling into a resource from AppHost code looks like.
    .WithAutomateResourceTerminalCommand("shell");
 
// Long-running container that the "Shell into container" interaction command execs into. Aspire is not orchestrating
// the exec — the AppHost shells out to `docker exec` — so the container needs a stable, predictable name.
var shellbox = builder.AddContainer("shellbox", "alpine")
    .WithContainerName("terminals-playground-shellbox")
    .WithArgs("sleep", "infinity")
    .WithContainerShellCommand()
    // Same shell, but delivered as a tab in the dashboard's terminal dock (backtick shortcut) rather than a modal dialog.
    .WithDockShellCommand();
 
// Latest Node.js image, kept alive so the "Node REPL" interaction command can exec into it. The Node REPL is a
// readline app, so it exercises cursor addressing, history, and tab completion across the tunnel in a way a plain
// shell prompt does not. `sleep infinity` replaces the image's default CMD ("node") — an interactive REPL with no TTY
// attached would exit immediately. The entrypoint (docker-entrypoint.sh) is left alone so PATH is set up normally.
builder.AddContainer("noderepl", "node", "latest")
    .WithContainerName("terminals-playground-noderepl")
    .WithArgs("sleep", "infinity")
    .WithNodeReplCommand();
 
if (OperatingSystem.IsWindows())
{
    // Local PowerShell launched directly by Hex1b, providing a Docker- and DCP-independent PTY debugging path.
    shellbox
        .WithPowerShellDockCommand()
        .WithCommandPromptDockCommand();
 
    // Single-replica executable wrapping cmd.exe to demonstrate that
    // WithTerminal() also works for arbitrary executables, not just projects.
    builder.AddExecutable("shell", "cmd.exe", ".")
        .WithTerminal(options => options.ShowTerminalHost = true);
 
    // A/B control resource: same cmd.exe but WITHOUT WithTerminal(). Used to
    // bisect whether the "Stop kills the dashboard" symptom is specific to
    // PTY-attached resources or applies to any DCP-managed Windows process.
    // Without a PTY, cmd.exe with redirected stdin would exit immediately, so
    // we wrap it in `/c ping -t 127.0.0.1` (continuous ping to localhost
    // until killed) so the resource stays in the Running state long enough
    // for "Stop" to be clicked from the dashboard.
    builder.AddExecutable("shell2", "cmd.exe", ".", "/c", "ping -t 127.0.0.1 > NUL");
 
    // Container resource exercising the container-side WithTerminal() path.
    // Launches a long-running Node.js LTS image with an interactive bash so
    // the terminal attaches to a shell where `npx`, `node`, and friends are
    // available. We override the image CMD (not the entrypoint!) so the
    // image's docker-entrypoint.sh keeps running and exec's bash for us;
    // overriding the entrypoint instead would leave the image's CMD
    // ("node") appended after bash and immediately exit.
    builder.AddContainer("nodebox", "node", "lts")
        .WithArgs("bash", "-l")
        .WithTerminal(options => options.ShowTerminalHost = true);
}
else
{
    // Unix counterpart of `shell`: an interactive login bash so the terminal
    // exercises the executable-with-PTY path on Linux/macOS. `-i` keeps bash
    // in interactive mode (prompt, line editing, job control) and `-l` makes
    // it source the user's login profile so `PATH`, aliases etc. behave the
    // same as a normal terminal session.
    builder.AddExecutable("shell", "/bin/bash", ".", "-i", "-l")
        .WithTerminal(options => options.ShowTerminalHost = true);
}
 
builder.AddDockerfile("notcurses", "../Terminals.Notcurses")
    .WithTerminal(options =>
    {
        // The demo recommends at least 80x45 for its graphics and Unicode workloads:
        // https://manpages.ubuntu.com/manpages/noble/man1/notcurses-demo.1.html
        options.Columns = 120;
        options.Rows = 45;
        options.ShowTerminalHost = true;
    });
 
#if !SKIP_DASHBOARD_REFERENCE
// This project is only added in playground projects to support development/debugging
// of the dashboard. It is not required in end developer code. Comment out this code
// or build with `/p:SkipDashboardReference=true`, to test end developer
// dashboard launch experience, Refer to Directory.Build.props for the path to
// the dashboard binary (defaults to the Aspire.Dashboard bin output in the
// artifacts dir).
builder.AddProject<Projects.Aspire_Dashboard>(KnownResourceNames.AspireDashboard);
#endif
 
builder.Build().Run();