diff --git a/.changeset/add-pi-web-terminal-environment.md b/.changeset/add-pi-web-terminal-environment.md new file mode 100644 index 00000000..16df1e5b --- /dev/null +++ b/.changeset/add-pi-web-terminal-environment.md @@ -0,0 +1,5 @@ +--- +"@jmfederico/pi-web": patch +--- + +Set `PI_WEB_TERMINAL=1` in PI WEB terminal shells. diff --git a/src/server/terminals/terminalService.test.ts b/src/server/terminals/terminalService.test.ts index 0b55d1ae..75f3957b 100644 --- a/src/server/terminals/terminalService.test.ts +++ b/src/server/terminals/terminalService.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; import type { RealtimeEvent, TerminalInfo } from "../../shared/apiTypes.js"; import type { WorkspaceActivityService } from "../activity/workspaceActivityService.js"; import { SessionEventHub } from "../realtime/sessionEventHub.js"; @@ -22,6 +22,73 @@ describe.skipIf(process.platform === "win32")("TerminalService command runs", () } }); + describe("PI_WEB_TERMINAL propagation", () => { + let originalPiWebTerminal: string | undefined; + + beforeEach(() => { + originalPiWebTerminal = process.env["PI_WEB_TERMINAL"]; + process.env["PI_WEB_TERMINAL"] = "conflicting-parent-value"; + }); + + afterEach(() => { + if (originalPiWebTerminal === undefined) { + delete process.env["PI_WEB_TERMINAL"]; + } else { + process.env["PI_WEB_TERMINAL"] = originalPiWebTerminal; + } + }); + + it("sets PI_WEB_TERMINAL for terminal commands", async () => { + const service = new TerminalService(); + try { + const frame = "__PI_WEB_RUN_ENV_7F3A9C__"; + const run = service.runCommand({ + origin: "core", + projectId: "p1", + workspaceId: "w1", + cwd: process.cwd(), + title: "Environment check", + command: `printf '${frame}%s${frame}\\n' "$PI_WEB_TERMINAL"`, + }); + + expect(await terminalExit(service, run.terminalId)).toContain(`${frame}1${frame}`); + } finally { + service.dispose(); + } + }); + + it("sets PI_WEB_TERMINAL in a continued interactive shell", async () => { + const service = new TerminalService(); + try { + const run = service.runCommand({ + origin: "core", + projectId: "p1", + workspaceId: "w1", + cwd: process.cwd(), + title: "Done command", + command: "true", + }); + await terminalExit(service, run.terminalId); + + const continued = service.continue(run.terminalId); + + expect(continued).toMatchObject({ id: run.terminalId, exited: false }); + expect(continued.commandRunId).toBeUndefined(); + expect(service.get(run.terminalId)?.commandRunId).toBeUndefined(); + + const frame = "__PI_WEB_CONTINUE_ENV_42D8B1__"; + const exit = terminalExit(service, run.terminalId); + service.write(run.terminalId, `printf '${frame}%s${frame}\\n' "$PI_WEB_TERMINAL"\nexit\n`); + + const output = await exit; + expect(output).toContain("[continued in interactive shell]"); + expect(output).toContain(`${frame}1${frame}`); + } finally { + service.dispose(); + } + }); + }); + it("tracks dedicated terminal command runs through completion", async () => { const service = new TerminalService(); try { @@ -50,30 +117,6 @@ describe.skipIf(process.platform === "win32")("TerminalService command runs", () } }); - it("continues an exited command-run terminal as an interactive shell", async () => { - const service = new TerminalService(); - try { - const run = service.runCommand({ - origin: "core", - projectId: "p1", - workspaceId: "w1", - cwd: process.cwd(), - title: "Done command", - command: "true", - }); - await terminalExit(service, run.terminalId); - - const continued = service.continue(run.terminalId); - - expect(continued).toMatchObject({ id: run.terminalId, exited: false }); - expect(continued.commandRunId).toBeUndefined(); - expect(service.get(run.terminalId)?.commandRunId).toBeUndefined(); - expect(await terminalReplay(service, run.terminalId)).toContain("[continued in interactive shell]"); - } finally { - service.dispose(); - } - }); - it("marks failed command runs when the command exits non-zero", async () => { const service = new TerminalService(); try { @@ -180,16 +223,6 @@ function requireTerminal(service: TerminalService, terminalId: string): Terminal return terminal; } -function terminalReplay(service: TerminalService, terminalId: string): Promise { - let output = ""; - const detach = service.attach(terminalId, { - output: (data) => { output += data; }, - exit: () => undefined, - }); - detach(); - return Promise.resolve(output); -} - function terminalExit(service: TerminalService, terminalId: string): Promise { const output: string[] = []; return new Promise((resolve, reject) => { diff --git a/src/server/terminals/terminalService.ts b/src/server/terminals/terminalService.ts index 63204ffd..7186893e 100644 --- a/src/server/terminals/terminalService.ts +++ b/src/server/terminals/terminalService.ts @@ -163,7 +163,7 @@ export class TerminalService { cwd: record.cwd, cols: 100, rows: 30, - env: { ...process.env, TERM: "xterm-256color" }, + env: { ...process.env, TERM: "xterm-256color", PI_WEB_TERMINAL: "1" }, }); this.attachPtyEvents(record); const info = toInfo(record); @@ -196,7 +196,7 @@ export class TerminalService { cwd: options.cwd, cols: options.cols ?? 100, rows: options.rows ?? 30, - env: { ...process.env, TERM: "xterm-256color" }, + env: { ...process.env, TERM: "xterm-256color", PI_WEB_TERMINAL: "1" }, }); const requestedName = options.name?.trim(); const record: TerminalRecord = {