diff --git a/apps/server/src/cli/servicePreflight.ts b/apps/server/src/cli/servicePreflight.ts index eefb2a35d37cb..9f01677141051 100644 --- a/apps/server/src/cli/servicePreflight.ts +++ b/apps/server/src/cli/servicePreflight.ts @@ -1,3 +1,4 @@ +import * as HostProcess from "@t3tools/shared/HostProcess"; import * as Console from "effect/Console"; import * as Effect from "effect/Effect"; import { Command, Flag } from "effect/cli"; @@ -10,8 +11,12 @@ export const servicePreflightCommand = Command.make("__service-preflight", { }).pipe( Command.unlisted, Command.withHandler(({ databasePath, launcherProtocol }) => - Console.log(JSON.stringify(runServicePreflight({ databasePath, launcherProtocol }))).pipe( - Effect.asVoid, + HostProcess.Platform.pipe( + Effect.flatMap((platform) => + Console.log( + JSON.stringify(runServicePreflight({ databasePath, launcherProtocol, platform })), + ), + ), ), ), ); diff --git a/apps/server/src/cloud/servicePreflight.test.ts b/apps/server/src/cloud/servicePreflight.test.ts index efc37266be8b0..c66b2070202a4 100644 --- a/apps/server/src/cloud/servicePreflight.test.ts +++ b/apps/server/src/cloud/servicePreflight.test.ts @@ -20,6 +20,7 @@ it.each([1])("blocks legacy launcher protocol %i with exact recovery steps", (la const result = runServicePreflight({ databasePath: "/missing/state.sqlite", launcherProtocol, + platform: "linux", version: "9.8.7", }); @@ -33,6 +34,25 @@ it.each([1])("blocks legacy launcher protocol %i with exact recovery steps", (la expect(reason).toContain("which -a t3"); }); +// regression: #34435 Windows hosts were told to run `curl | sh` and `~/.local/bin/t3 service install` +it("gives Windows hosts recovery steps that run on Windows", () => { + const result = runServicePreflight({ + databasePath: "C:\\missing\\state.sqlite", + launcherProtocol: 3, + platform: "win32", + version: "9.8.7", + }); + const reason = result.status === "blocked" ? result.reason : ""; + expect(reason).toMatch(new RegExp(`\\b${SERVICE_LAUNCHER_PROTOCOL}\\b`)); + expect(reason).toContain("9.8.7"); + expect(reason).not.toContain("| sh"); + expect(reason).not.toContain("which -a"); + expect(reason).not.toContain("~/.local/bin/t3"); + expect(reason).toContain( + `service-state.json" -Value '{"protocol":${SERVICE_LAUNCHER_PROTOCOL},"activeVersion":"9.8.7"}'`, + ); +}); + it.layer(NodeServices.layer)("service preflight database", (it) => { it.effect("blocks a database that a newer build migrated", () => Effect.gen(function* () { @@ -47,7 +67,11 @@ it.layer(NodeServices.layer)("service preflight database", (it) => { const insert = db.prepare("INSERT INTO effect_sql_migrations VALUES (?, ?)"); for (const [id, name] of migrationManifest) insert.run(id, name); const preflight = () => - runServicePreflight({ databasePath, launcherProtocol: SERVICE_LAUNCHER_PROTOCOL }); + runServicePreflight({ + databasePath, + launcherProtocol: SERVICE_LAUNCHER_PROTOCOL, + platform: "linux", + }); expect(preflight().status).toBe("ready"); expect(databaseMigrationHead(databasePath)).toBe(BUILD_MIGRATION_HEAD); @@ -69,6 +93,7 @@ it.layer(NodeServices.layer)("service preflight database", (it) => { runServicePreflight({ databasePath: unreadable, launcherProtocol: SERVICE_LAUNCHER_PROTOCOL, + platform: "linux", }).status, ).toBe("blocked"); // A missing database is a fresh install. diff --git a/apps/server/src/cloud/servicePreflight.ts b/apps/server/src/cloud/servicePreflight.ts index 9479f3f4a8194..2e808440e8f5a 100644 --- a/apps/server/src/cloud/servicePreflight.ts +++ b/apps/server/src/cloud/servicePreflight.ts @@ -17,6 +17,7 @@ export type ServicePreflightResult = export function runServicePreflight(input: { readonly databasePath: string; readonly launcherProtocol: number; + readonly platform: NodeJS.Platform; readonly version?: string; }): ServicePreflightResult { const version = input.version ?? packageJson.version; @@ -28,9 +29,21 @@ export function runServicePreflight(input: { // reinstall itself and report success, so name the exact install and the path check. reason: [ `The installed service launcher speaks protocol ${input.launcherProtocol}, but t3@${version} requires protocol ${SERVICE_LAUNCHER_PROTOCOL}.`, - `On the server machine, install this exact CLI: \`curl -fsSL https://t3.codes/install.sh | T3CODE_VERSION=${version} sh\`.`, - `Check that \`t3 --version\` prints ${version} and that \`which -a t3\` lists no older t3 (such as an npm or nvm install) first, then run \`~/.local/bin/t3 service install\` (the installer's default path).`, - "Updating the desktop app does not update this CLI, and `t3 service update` only repairs the service with whichever CLI runs it.", + ...(input.platform === "win32" + ? [ + `On the server machine, install this exact CLI in PowerShell: \`$env:T3CODE_VERSION="${version}"; irm https://t3.codes/install.ps1 | iex\`.`, + `Check that \`t3 --version\` prints ${version} and that \`where.exe t3\` lists no older t3 first.`, + // Without `t3 service install`, service-state.json still selects the old server, which + // rejects the new launcher's context; select the installed runtime by hand. + `Windows has no \`t3 service install\`, so do its steps by hand: stop whatever runs \`t3 __service-launcher\`, then select this runtime and clear the stale update record: \`Set-Content -LiteralPath "$HOME\\.t3\\runtime\\service-state.json" -Value '{"protocol":${SERVICE_LAUNCHER_PROTOCOL},"activeVersion":"${version}"}'\` (use your T3CODE_HOME in place of \`$HOME\\.t3\` if you set one).`, + `Start the launcher again from \`$HOME\\.local\\bin\\t3.cmd\` (the installer's default path). Nothing moves a Windows launcher forward after a remote update, so repeat these steps whenever a release changes the launcher protocol.`, + "Updating the desktop app does not update this CLI.", + ] + : [ + `On the server machine, install this exact CLI: \`curl -fsSL https://t3.codes/install.sh | T3CODE_VERSION=${version} sh\`.`, + `Check that \`t3 --version\` prints ${version} and that \`which -a t3\` lists no older t3 (such as an npm or nvm install) first, then run \`~/.local/bin/t3 service install\` (the installer's default path).`, + "Updating the desktop app does not update this CLI, and `t3 service update` only repairs the service with whichever CLI runs it.", + ]), ].join(" "), }; }