Skip to content
Draft
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
19 changes: 18 additions & 1 deletion src/commands/info.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import chalk from 'chalk';

import { ApifyCommand } from '../lib/command-framework/apify-command.js';
import { getLocalUserInfo, getLoggedClientOrThrow } from '../lib/utils.js';
import { getLocalUserInfo, getLoggedClientOrThrow, printJsonToStdout } from '../lib/utils.js';

export class InfoCommand extends ApifyCommand<typeof InfoCommand> {
static override name = 'info' as const;
Expand All @@ -15,13 +15,30 @@ export class InfoCommand extends ApifyCommand<typeof InfoCommand> {
description: 'Print the currently logged-in account username and user ID.',
command: 'apify info',
},
{
description: 'Print the currently logged-in account as JSON.',
command: 'apify info --json',
},
];

static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-info';

static override enableJsonFlag = true;

async run() {
await getLoggedClientOrThrow();
const info = await getLocalUserInfo();
const { json } = this.flags;

if (json) {
// Never leak the raw API token in --json output — it's a secret and scripts routinely
// log their JSON responses. Emit only the safe identity fields.
printJsonToStdout({
username: info?.username ?? null,
userId: info?.id ?? null,
});
return;
}

if (info) {
const niceInfo = {
Expand Down
33 changes: 32 additions & 1 deletion src/entrypoints/_shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { CommandError } from '../lib/command-framework/CommandError.js';
import { renderMainHelpMenu } from '../lib/command-framework/help.js';
import { readStdin } from '../lib/commands/read-stdin.js';
import { SUPPORTED_NODEJS_VERSION } from '../lib/consts.js';
import { AuthError } from '../lib/errors/AuthError.js';
import { useCLIMetadata } from '../lib/hooks/useCLIMetadata.js';
import { shouldSkipVersionCheck } from '../lib/hooks/useCLIVersionCheck.js';
import { useCommandSuggestions } from '../lib/hooks/useCommandSuggestions.js';
Expand Down Expand Up @@ -248,10 +249,40 @@ export async function runCLI(entrypoint: string) {

cliDebugPrint('CommandArgsResult', commandResult);
} catch (err) {
// Auth failures deserve a machine-readable envelope when the caller opted into
// structured output with --json; otherwise scripts piping `apify ... --json | jq`
// blow up on the human-facing prose that would normally go to stderr.
if (err instanceof AuthError && jsonFlagRequested(FinalCommand, rebuiltArgs)) {
const envelope = {
error: {
type: err.type,
message: err.message,
exitCode: err.exitCode,
},
};

console.log(JSON.stringify(envelope, null, 2));
process.exit(err.exitCode);
}

const commandError = CommandError.into(err, FinalCommand);

error({ message: commandError.getPrettyMessage() });

process.exit(1);
// AuthError sets process.exitCode via CommandExitCodes.MissingAuth upstream — surface it
// (rather than the hard-coded 1) so callers can distinguish auth failures from other errors.
const exitCode = err instanceof AuthError ? err.exitCode : 1;
process.exit(exitCode);
}
}

/**
* Best-effort check for whether `--json` was requested for this invocation. We can't rely on
* `instance.flags.json` because errors can be thrown from `parseArgs` itself (before flags are
* populated) or from inside `_run` before the flags object is fully hydrated. Scanning the raw
* argv slice is cheap and matches the user's actual intent to receive structured output.
*/
function jsonFlagRequested(command: typeof BuiltApifyCommand, args: string[]): boolean {
if (!command.enableJsonFlag) return false;
return args.includes('--json') || args.includes('--json=true');
}
23 changes: 23 additions & 0 deletions src/lib/errors/AuthError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { CommandExitCodes } from '../consts.js';

/**
* Thrown when an Apify command requires authentication but the user is not logged in
* (or the stored credentials are unusable).
*
* The top-level CLI handler uses this class to decide between:
* - printing a human-friendly "please run apify login" message on stderr, or
* - emitting a machine-readable JSON envelope on stdout when the invocation
* set `--json` (so scripts that pipe `apify ... --json | jq ...` see a
* parseable object instead of prose).
*/
export class AuthError extends Error {
public readonly type = 'AuthError' as const;

public readonly exitCode: number;

public constructor(message: string, exitCode: number = CommandExitCodes.MissingAuth) {
super(message);
this.name = 'AuthError';
this.exitCode = exitCode;
}
}
6 changes: 5 additions & 1 deletion src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
SUPPORTED_NODEJS_VERSION,
} from './consts.js';
import { ensureMigrated, getBackend, getProxyPassword, getToken, setProxyPassword, setToken } from './credentials.js';
import { AuthError } from './errors/AuthError.js';
import { deleteFile, ensureApifyDirectory, ensureFolderExistsSync, rimrafPromised } from './files.js';
import { inputFileRegExp, TEMP_INPUT_KEY_PREFIX } from './input-key.js';
import type { AuthJSON } from './types.js';
Expand Down Expand Up @@ -141,7 +142,10 @@ export async function getLoggedClientOrThrow() {

if (!loggedClient) {
process.exitCode = CommandExitCodes.MissingAuth;
throw new Error('You are not logged in with your Apify account. Call "apify login" to fix that.');
throw new AuthError(
'You are not logged in with your Apify account. Call "apify login" to fix that.',
CommandExitCodes.MissingAuth,
);
}
return loggedClient;
}
Expand Down
Loading