diff --git a/src/cli.test.ts b/src/cli.test.ts index 613b419..187fc7e 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -32,6 +32,7 @@ const parseJsonOutput = (value: string) => JSON.parse(value) as Record; + readonly isSingleExecutable?: boolean; }; const runProcessArgv = async ( @@ -71,6 +72,7 @@ const runProcessArgv = async ( makeCliRuntime({ argv: processArgv, homeDirectory: configDir, + isSingleExecutable: options.isSingleExecutable, writeStdout: (message) => { stdoutChunks.push(message.trim()); }, @@ -159,6 +161,16 @@ describe("cli argv parsing", () => { expect(stdout).toBe(`putio v${packageJson.version}`); }); + it("accepts SEA argv regardless of the executable name", async () => { + const { result, stdout } = await runProcessArgv( + ["/opt/putio/bin/putio-darwin-arm64", "./putio-darwin-arm64", "version", "--output", "json"], + { isSingleExecutable: true }, + ); + + expect(result._tag).toBe("Success"); + expect(parseJsonOutput(stdout)).toMatchObject({ version: packageJson.version }); + }); + it("renders describe as machine-readable json", async () => { const { result, stdout } = await runCli(["putio", "describe"]); diff --git a/src/cli.ts b/src/cli.ts index 8d4b808..8dc72a4 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -127,14 +127,15 @@ const stripLeadingPutioExecutables = (args: ReadonlyArray) => { return args.slice(start); }; -const commandArgsFromArgv = (args: ReadonlyArray) => { +const commandArgsFromArgv = (args: ReadonlyArray, isSingleExecutable: boolean) => { const [first] = args; if (first === undefined) { return args; } - if (isNodeExecutable(first)) { + // A single executable application puts its own path in argv[0] and the invoked name in argv[1]. + if (isSingleExecutable || isNodeExecutable(first)) { return stripLeadingPutioExecutables(args.slice(2)); } @@ -160,7 +161,7 @@ export const runCli: ( }); const runtime = yield* CliRuntime; const outputMode = detectOutputModeFromArgv(args, runtime.isInteractiveTerminal); - const commandArgs = commandArgsFromArgv(args); + const commandArgs = commandArgsFromArgv(args, runtime.isSingleExecutable); yield* Effect.annotateCurrentSpan({ "cli.command": commandArgs[0] ?? "root", diff --git a/src/internal/auth-flow.test.ts b/src/internal/auth-flow.test.ts index 5710cfd..df3c08e 100644 --- a/src/internal/auth-flow.test.ts +++ b/src/internal/auth-flow.test.ts @@ -18,6 +18,7 @@ const mockRuntime: CliRuntimeService = { getHomeDirectory: Effect.succeed("/tmp"), getHostname: Effect.succeed("putio-host"), isInteractiveTerminal: false, + isSingleExecutable: false, joinPath: (...segments) => segments.join("/"), openExternal: (_url) => Effect.succeed(true), setExitCode: (_code) => Effect.void, diff --git a/src/internal/runtime.ts b/src/internal/runtime.ts index 47e2c1e..2aad51d 100644 --- a/src/internal/runtime.ts +++ b/src/internal/runtime.ts @@ -1,6 +1,7 @@ import { spawn } from "node:child_process"; import { homedir, hostname } from "node:os"; import { dirname, join } from "node:path"; +import { isSea } from "node:sea"; import { Context, Effect, Layer } from "effect"; @@ -11,6 +12,7 @@ type CliSpinner = { export type CliRuntimeService = { readonly argv: ReadonlyArray; readonly isInteractiveTerminal: boolean; + readonly isSingleExecutable: boolean; readonly setExitCode: (code: number) => Effect.Effect; readonly writeStdout: (message: string) => Effect.Effect; readonly writeStderr: (message: string) => Effect.Effect; @@ -121,6 +123,7 @@ export const makeCliRuntime = ( options: { readonly argv?: ReadonlyArray; readonly isInteractiveTerminal?: boolean; + readonly isSingleExecutable?: boolean; readonly platform?: NodeJS.Platform; readonly homeDirectory?: string; readonly hostName?: string; @@ -134,11 +137,13 @@ export const makeCliRuntime = ( const hostName = options.hostName ?? hostname(); const isInteractiveTerminal = options.isInteractiveTerminal ?? Boolean(process.stdout.isTTY && process.stdin.isTTY); + const isSingleExecutable = options.isSingleExecutable ?? isSea(); const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const; return { argv, isInteractiveTerminal, + isSingleExecutable, setExitCode: (code) => Effect.sync(() => { process.exitCode = code;