Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const parseJsonOutput = (value: string) => JSON.parse(value) as Record<string, u
type CliRunOptions = {
readonly configContents?: string;
readonly env?: Record<string, string>;
readonly isSingleExecutable?: boolean;
};

const runProcessArgv = async (
Expand Down Expand Up @@ -71,6 +72,7 @@ const runProcessArgv = async (
makeCliRuntime({
argv: processArgv,
homeDirectory: configDir,
isSingleExecutable: options.isSingleExecutable,
writeStdout: (message) => {
stdoutChunks.push(message.trim());
},
Expand Down Expand Up @@ -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"]);

Expand Down
7 changes: 4 additions & 3 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,15 @@ const stripLeadingPutioExecutables = (args: ReadonlyArray<string>) => {
return args.slice(start);
};

const commandArgsFromArgv = (args: ReadonlyArray<string>) => {
const commandArgsFromArgv = (args: ReadonlyArray<string>, 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));
}

Expand All @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/internal/auth-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions src/internal/runtime.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -11,6 +12,7 @@ type CliSpinner = {
export type CliRuntimeService = {
readonly argv: ReadonlyArray<string>;
readonly isInteractiveTerminal: boolean;
readonly isSingleExecutable: boolean;
readonly setExitCode: (code: number) => Effect.Effect<void>;
readonly writeStdout: (message: string) => Effect.Effect<void>;
readonly writeStderr: (message: string) => Effect.Effect<void>;
Expand Down Expand Up @@ -121,6 +123,7 @@ export const makeCliRuntime = (
options: {
readonly argv?: ReadonlyArray<string>;
readonly isInteractiveTerminal?: boolean;
readonly isSingleExecutable?: boolean;
readonly platform?: NodeJS.Platform;
readonly homeDirectory?: string;
readonly hostName?: string;
Expand All @@ -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;
Expand Down
Loading