diff --git a/cli/src/__tests__/onboard-service.test.ts b/cli/src/__tests__/onboard-service.test.ts index 5b3f406433..1b2b6baf4a 100644 --- a/cli/src/__tests__/onboard-service.test.ts +++ b/cli/src/__tests__/onboard-service.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { handleOnboardService, isInstallableReleaseVersion } from "../onboard-service.js"; +import { handleOnboardService, isInstallableReleaseVersion, shouldOfferForegroundStart } from "../onboard-service.js"; function supportedDetection() { return { @@ -138,3 +138,27 @@ describe("isInstallableReleaseVersion", () => { expect(isInstallableReleaseVersion("not-a-version")).toBe(false); }); }); + +describe("shouldOfferForegroundStart", () => { + const base = { serviceInstalled: false, startAlreadyDecided: false, invokedByRun: false, interactive: true }; + + it("offers a foreground start on a plain interactive onboard", () => { + expect(shouldOfferForegroundStart(base)).toBe(true); + }); + + it("never prompts after the service was installed and started", () => { + expect(shouldOfferForegroundStart({ ...base, serviceInstalled: true })).toBe(false); + }); + + it("never prompts when the start decision was already made by flags", () => { + expect(shouldOfferForegroundStart({ ...base, startAlreadyDecided: true })).toBe(false); + }); + + it("never prompts when run itself invoked onboarding", () => { + expect(shouldOfferForegroundStart({ ...base, invokedByRun: true })).toBe(false); + }); + + it("never prompts without an interactive terminal", () => { + expect(shouldOfferForegroundStart({ ...base, interactive: false })).toBe(false); + }); +}); diff --git a/cli/src/commands/onboard.ts b/cli/src/commands/onboard.ts index 31850714d7..db94cb24a7 100644 --- a/cli/src/commands/onboard.ts +++ b/cli/src/commands/onboard.ts @@ -52,7 +52,7 @@ import { trackInstallStarted, trackInstallCompleted, } from "../telemetry.js"; -import { handleOnboardService } from "../onboard-service.js"; +import { handleOnboardService, shouldOfferForegroundStart } from "../onboard-service.js"; import { readInstallManifest, isManagedExecutable } from "../install-store.js"; type SetupMode = "quickstart" | "advanced"; @@ -459,7 +459,7 @@ export async function onboard(opts: OnboardOptions): Promise { const serviceInstalled = await handleOnboardService(opts); let shouldRunNow = !serviceInstalled && (opts.run === true || opts.yes === true); - if (!shouldRunNow && !opts.invokedByRun && process.stdin.isTTY && process.stdout.isTTY) { + if (shouldOfferForegroundStart({ serviceInstalled, startAlreadyDecided: shouldRunNow, invokedByRun: opts.invokedByRun === true, interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY) })) { const answer = await p.confirm({ message: "Start Paperclip now?", initialValue: true, @@ -725,7 +725,7 @@ export async function onboard(opts: OnboardOptions): Promise { const serviceInstalled = await handleOnboardService(opts); let shouldRunNow = !serviceInstalled && (opts.run === true || opts.yes === true); - if (!shouldRunNow && !opts.invokedByRun && process.stdin.isTTY && process.stdout.isTTY) { + if (shouldOfferForegroundStart({ serviceInstalled, startAlreadyDecided: shouldRunNow, invokedByRun: opts.invokedByRun === true, interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY) })) { const answer = await p.confirm({ message: "Start Paperclip now?", initialValue: true, diff --git a/cli/src/onboard-service.ts b/cli/src/onboard-service.ts index 268f121e51..1f52e970aa 100644 --- a/cli/src/onboard-service.ts +++ b/cli/src/onboard-service.ts @@ -165,3 +165,20 @@ export async function handleOnboardService( deps.success(`Installed and started ${detection.manager.serviceName}.`); return true; } + +// Onboarding falls back to offering a foreground start when nothing else +// will serve. A just-installed service is already serving, so offering the +// start would only run the user into the already-running instance guard. +export function shouldOfferForegroundStart(options: { + serviceInstalled: boolean; + startAlreadyDecided: boolean; + invokedByRun: boolean; + interactive: boolean; +}): boolean { + return ( + !options.startAlreadyDecided && + !options.serviceInstalled && + !options.invokedByRun && + options.interactive + ); +}