Skip to content

Commit cd0e104

Browse files
only21milclaude
andauthored
fix(cli): skip the service prompt when t3 connect has no terminal (#23738)
Co-authored-by: Claude Opus 5.5 <[email protected]>
1 parent be43da4 commit cd0e104

2 files changed

Lines changed: 62 additions & 0 deletions

File tree

‎apps/server/src/cli/service.test.ts‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,60 @@ it.effect("leaves a newer service unchanged during onboarding without prompting"
182182
}),
183183
);
184184

185+
const withTerminal = (isTTY: { readonly stdin: boolean; readonly stdout: boolean }) =>
186+
Effect.acquireRelease(
187+
Effect.sync(() =>
188+
(
189+
[
190+
[process.stdin, isTTY.stdin],
191+
[process.stdout, isTTY.stdout],
192+
] as const
193+
).map(([stream, value]) => {
194+
const descriptor = Object.getOwnPropertyDescriptor(stream, "isTTY");
195+
Object.defineProperty(stream, "isTTY", { configurable: true, value });
196+
return { stream, descriptor };
197+
}),
198+
),
199+
(saved) =>
200+
Effect.sync(() => {
201+
for (const { stream, descriptor } of saved) {
202+
if (descriptor === undefined) Reflect.deleteProperty(stream, "isTTY");
203+
else Object.defineProperty(stream, "isTTY", descriptor);
204+
}
205+
}),
206+
);
207+
208+
it.effect.each([
209+
{ label: "stdin and stdout are", stdin: false, stdout: false },
210+
{ label: "stdin is", stdin: false, stdout: true },
211+
{ label: "stdout is", stdin: true, stdout: false },
212+
])("skips the onboarding service prompt when $label not a terminal", (isTTY) =>
213+
Effect.gen(function* () {
214+
yield* withTerminal(isTTY);
215+
const { service, installOptions } = makeTestService({
216+
...status,
217+
installed: false,
218+
current: false,
219+
});
220+
const terminal = Terminal.make({
221+
columns: Effect.succeed(80),
222+
rows: Effect.succeed(24),
223+
readInput: Effect.die("Onboarding must not prompt without a terminal."),
224+
readLine: Effect.die("Onboarding must not prompt without a terminal."),
225+
display: () => Effect.die("Onboarding must not prompt without a terminal."),
226+
});
227+
228+
const ready = yield* offerServiceDuringOnboarding.pipe(
229+
Effect.provideService(BootService.BootService, service),
230+
Effect.provideService(Terminal.Terminal, terminal),
231+
Effect.provide(NodeServices.layer),
232+
);
233+
234+
expect(ready).toBe("skipped");
235+
expect(installOptions).toEqual([]);
236+
}),
237+
);
238+
185239
// regression: #9602 diagnose incomplete headless server setup
186240
it.effect("keeps the manual-server fallback when background prerequisites fail", () =>
187241
Effect.gen(function* () {

‎apps/server/src/cli/service.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,14 @@ export const offerServiceDuringOnboarding = Effect.gen(function* () {
379379
);
380380
return "skipped" as const;
381381
}
382+
// Both streams must be terminals: with stdout redirected the prompt is
383+
// invisible and the command would hang waiting on it.
384+
if (!(process.stdin.isTTY && process.stdout.isTTY)) {
385+
yield* Console.log(
386+
"Not a terminal, so background setup was skipped. Run `t3 service install` to set it up.",
387+
);
388+
return "skipped" as const;
389+
}
382390
const platform = yield* HostProcess.Platform;
383391
const wanted = yield* Prompt.run(
384392
Prompt.Confirm({

0 commit comments

Comments
 (0)