Public: anyone can read this; the crew can change it.

Every app on a hosted forge says “The terminal is off.” and never why or what turns it on #318

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/283-every-app-on-a-hosted-forge-says-the-ter → main 1 commit fork

  • Merged. scout/283-every-app-on-a-hosted-forge-says-the-ter was merged into main as 38fbc21.

Review

Sign in to review this change.
4 files changed +56 −13
src/actions/apps.ts +13 −8
@@ -44,6 +44,7 @@ import type { Run } from "../core/runs.ts";
4444 import type { User } from "../core/users.ts";
4545 import { resolveOid } from "../git/merge.ts";
4646 import { repoDir } from "../git/repos.ts";
47 import { sshCommand } from "../ssh/command.ts";
4748 import { err, ok, type Result } from "../kernel/result.ts";
4849 import {
4950 requireReader,
@@ -378,20 +379,24 @@ export function sshCommandFor(
378379 }
379380
380381 /**
381 * The one fact behind every "Terminal" line a page draws: the exact command,
382 * or undefined when there is nothing to show — SSH is off on this forge, or
383 * this environment's app declares no `tty`. A status that has never been
384 * live does not know its `tty` yet, so the command still shows; the
385 * environment's own state already says it is not running.
382 * The one fact behind every "Terminal" line a page draws: the exact
383 * command, or — when there is nothing to show — why: this environment's
384 * app declares no `tty`, or this forge itself has no SSH listener
385 * (`app.config.sshPort`, `0` on a forge hosted as an app of another —
386 * issue #283). A status that has never been live does not know its `tty`
387 * yet, so the command still shows when SSH is on; the environment's own
388 * state already says it is not running.
386389 */
387390 export function terminalFor(
388391 app: App,
389392 record: AppRecord,
390393 env: EnvName,
391394 status: EnvStatus | undefined,
392 ): string | undefined {
393 if (status?.tty === null) return undefined;
394 return sshCommandFor(app, record, env);
395 ): { readonly command?: string; readonly reason?: "no_tty" | "no_listener" } {
396 if (status?.tty === null) return { reason: "no_tty" };
397 const command = sshCommandFor(app, record, env);
398 if (command !== undefined) return { command };
399 return sshCommand(app) === undefined ? { reason: "no_listener" } : {};
395400 }
396401
397402 /** What is running, for anyone who can see the repository. */
src/web/pages/apps.ts +1 −1
@@ -637,7 +637,7 @@ export function appPage(ctx: Ctx): Response {
637637 const production = envs.find((env) => env.env === "production");
638638 const ssh = writer
639639 ? terminalFor(ctx.app, record, "production", production)
640 : undefined;
640 : {};
641641 // The same rows the runs page draws, from the same two grouped reads: a
642642 // table's first column folds into a sliver at phone width, a row does not.
643643 const runsHead = html`${icon("activity")}
src/web/ui.ts +20 −4
@@ -418,6 +418,14 @@ export function previewDataNote(
418418 * show. Every page that names a way in draws this one, never its own words
419419 * for "off".
420420 *
421 * "Off" has two causes and a reader deserves to know which: the app
422 * declares no `tty` (nothing would answer that connection even if one were
423 * open — the sentence stays exactly what it always said), or this forge has
424 * no SSH listener (`FORGE_SSH_PORT`, `0` on a forge hosted as an app of
425 * another — issue #283) and the app is otherwise willing. The second names
426 * the variable that turns it on; neither ever invents a connection string —
427 * a command is shown only once there is a listener to reach.
428 *
421429 * The command has a `login@host` in it, which an email-scanning edge on the
422430 * forge's own domain will otherwise rewrite into a `mailto:` link — turning
423431 * the one line a reader must copy character for character into a link that
@@ -427,13 +435,21 @@ export function previewDataNote(
427435 * copyable line.
428436 */
429437 export function terminalLine(
430 command: string | undefined,
438 terminal: {
439 readonly command?: string;
440 readonly reason?: "no_tty" | "no_listener";
441 },
431442 fingerprint?: string | undefined,
432443 ): Html {
433 if (command === undefined)
434 return html`<p class="muted small">The terminal is off.</p>`;
444 if (terminal.command === undefined)
445 return terminal.reason === "no_listener"
446 ? html`<p class="muted small">
447 The terminal is off — this forge has no SSH listener. Set
448 <span class="mono">FORGE_SSH_PORT</span> to turn it on.
449 </p>`
450 : html`<p class="muted small">The terminal is off.</p>`;
435451 return html`<p class="terminal-line">
436 <code class="mono"><!--email_off-->${command}<!--/email_off--></code>
452 <code class="mono"><!--email_off-->${terminal.command}<!--/email_off--></code>
437453 ${when(
438454 fingerprint,
439455 (fp) => html`<span class="muted small">host key ${fp}</span>`,
test/studio.test.ts +22
@@ -2888,6 +2888,28 @@ describe("the studio app page's terminal", () => {
28882888 const theirs = await appPage(reader);
28892889 expect(theirs).not.toContain("The terminal is off");
28902890 });
2891
2892 // Issue #283: a hosted forge (an app of another forge) defaults
2893 // FORGE_SSH_PORT to 0, so serve() never opens a listener — every app on
2894 // it declares a tty but has nothing to connect it to. The generic "The
2895 // terminal is off." leaves a reader with no idea why or what to do; this
2896 // asserts the line names FORGE_SSH_PORT instead, composed from the same
2897 // sshPort field the other tests here already pin (#77's ground) rather
2898 // than by shutting a real listener down.
2899 test("a hosted forge with no SSH listener says why, and names FORGE_SSH_PORT", async () => {
2900 putLive("bun run src/tty.ts");
2901 app.config.sshPort = 0;
2902 try {
2903 const mine = await appPage(writer);
2904 expect(mine).toContain("FORGE_SSH_PORT");
2905 expect(mine).not.toContain("ssh -p 2222 [email protected]");
2906 expect(mine).not.toContain(
2907 "The terminal is off.</p>",
2908 );
2909 } finally {
2910 app.config.sshPort = 2222;
2911 }
2912 });
28912913 });
28922914
28932915 /**

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs