Skip to content
Open
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
62 changes: 49 additions & 13 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -574,19 +574,20 @@ DESCRIPTION
Manages Actor creation, deployment, and execution on the Apify platform.

SUBCOMMANDS
actors start Starts Actor remotely and returns run details
immediately.
actors rm Permanently removes an Actor from your account.
actors push Deploys Actor to Apify platform using settings from
'.actor/actor.json'.
actors pull Download Actor code to current directory. Clones Git
repositories or fetches Actor files based on the source type.
actors ls Prints a list of recently executed Actors or Actors
you own.
actors info Get information about an Actor.
actors call Executes Actor remotely using your authenticated
account.
actors build Creates a new build of the Actor.
actors start Starts Actor remotely and returns run details
immediately.
actors rm Permanently removes an Actor from your account.
actors search Searches Actors in the Apify Store.
actors push Deploys Actor to Apify platform using settings
from '.actor/actor.json'.
actors pull Download Actor code to current directory. Clones
Git repositories or fetches Actor files based on the source type.
actors ls Prints a list of recently executed Actors or
Actors you own.
actors info Get information about an Actor.
actors call Executes Actor remotely using your authenticated
account.
actors build Creates a new build of the Actor.
```

##### `apify actors ls`
Expand All @@ -608,6 +609,41 @@ FLAGS
--offset=<value> Number of Actors that will be skipped.
```

##### `apify actors search`

```sh
DESCRIPTION
Searches Actors in the Apify Store.

Searches the Apify Store for Actors matching the given query. Results can be
filtered by category, author, pricing model, and more. This command does not
require authentication.

USAGE
$ apify actors search [query] [--category <value>]
[--json] [--limit <value>] [--offset <value>]
[--pricing-model <value>] [--sort-by <value>]
[--username <value>]

ARGUMENTS
query Search query to find Actors by title, name, description, username,
or readme.

FLAGS
--category=<value> Filter by category (e.g.
AI).
--json Format the command output as
JSON
--limit=<value> Maximum number of results to
return.
--offset=<value> Number of results to skip
for pagination.
--pricing-model=<value> Filter by pricing model.
--sort-by=<value> Sort order for the results.
--username=<value> Filter by Actor author
username.
```

##### `apify actors rm`

```sh
Expand Down
1 change: 1 addition & 0 deletions scripts/generate-cli-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const categories: Record<string, CommandsInCategory[]> = {
//
{ command: Commands.actors },
{ command: Commands.actorsLs },
{ command: Commands.actorsSearch },
{ command: Commands.actorsRm },
],
'actor-deploy': [
Expand Down
2 changes: 2 additions & 0 deletions src/commands/actors/_index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ActorsLsCommand } from './ls.js';
import { ActorsPullCommand } from './pull.js';
import { ActorsPushCommand } from './push.js';
import { ActorsRmCommand } from './rm.js';
import { ActorsSearchCommand } from './search.js';
import { ActorsStartCommand } from './start.js';

export class ActorsIndexCommand extends ApifyCommand<typeof ActorsIndexCommand> {
Expand All @@ -17,6 +18,7 @@ export class ActorsIndexCommand extends ApifyCommand<typeof ActorsIndexCommand>
//
ActorsStartCommand,
ActorsRmCommand,
ActorsSearchCommand,
ActorsPushCommand,
ActorsPullCommand,
ActorsLsCommand,
Expand Down
140 changes: 140 additions & 0 deletions src/commands/actors/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import type { ActorStoreList } from 'apify-client';
import { ApifyClient } from 'apify-client';
import chalk from 'chalk';

import { ApifyCommand } from '../../lib/command-framework/apify-command.js';
import { Args } from '../../lib/command-framework/args.js';
import { Flags } from '../../lib/command-framework/flags.js';
import { CompactMode, ResponsiveTable } from '../../lib/commands/responsive-table.js';
import { CommandExitCodes } from '../../lib/consts.js';
import { error, info, simpleLog } from '../../lib/outputs.js';
import { getApifyClientOptions, printJsonToStdout } from '../../lib/utils.js';

const pricingModelLabels: Record<string, string> = {
FREE: 'Free',
FLAT_PRICE_PER_MONTH: 'Subscription',
PRICE_PER_DATASET_ITEM: 'Pay per result',
PAY_PER_EVENT: 'Pay per event',
};

function formatPricingModel(model?: string): string {
if (!model) return chalk.gray('Unknown');

return pricingModelLabels[model] ?? model;
}

function truncateDescription(description?: string, maxLength = 60): string {
if (!description) return '';

if (description.length <= maxLength) return description;

return `${description.slice(0, maxLength - 1)}…`;
}

export class ActorsSearchCommand extends ApifyCommand<typeof ActorsSearchCommand> {
static override name = 'search' as const;

static override description =
'Searches Actors in the Apify Store.\n\nSearches the Apify Store for Actors matching the given query. Results can be filtered by category, author, pricing model, and more. This command does not require authentication.';

static override args = {
query: Args.string({
description: 'Search query to find Actors by title, name, description, username, or readme.',
required: false,
}),
};

static override flags = {
'sort-by': Flags.string({
description: 'Sort order for the results.',
options: ['relevance', 'popularity', 'newest', 'lastUpdate'],
default: 'relevance',
}),
category: Flags.string({
description: 'Filter by category (e.g. AI).',
}),
username: Flags.string({
description: 'Filter by Actor author username.',
}),
'pricing-model': Flags.string({
description: 'Filter by pricing model.',
options: ['FREE', 'FLAT_PRICE_PER_MONTH', 'PRICE_PER_DATASET_ITEM', 'PAY_PER_EVENT'],
}),
limit: Flags.integer({
description: 'Maximum number of results to return.',
default: 20,
}),
offset: Flags.integer({
description: 'Number of results to skip for pagination.',
default: 0,
}),
};

static override enableJsonFlag = true;

async run() {
const { query } = this.args;
const { json, sortBy, category, username, pricingModel, limit, offset } = this.flags;

const client = new ApifyClient(getApifyClientOptions());

let result;

try {
result = await client.store().list({
search: query,
sortBy,
category,
username,
pricingModel,
limit,
offset,
});
} catch (err) {
process.exitCode = CommandExitCodes.RunFailed;
error({
message: `Failed to search Apify Store: ${err instanceof Error ? err.message : String(err)}`,
stdout: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vladfrangu stdout or stderr ? 🤔

});
return;
}

if (result.count === 0) {
if (json) {
printJsonToStdout(result);
return;
}

info({ message: 'No Actors found matching your search.', stdout: true });
return;
}

if (json) {
printJsonToStdout(result);
return;
}

const table = new ResponsiveTable({
allColumns: ['Name', 'Description', 'Users (30d)', 'Pricing'],
mandatoryColumns: ['Name', 'Pricing'],
columnAlignments: {
'Users (30d)': 'right',
Name: 'left',
},
});

for (const item of result.items as ActorStoreList[]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for (const item of result.items as ActorStoreList[]) {
for (const item of result.items) {

table.pushRow({
Name: `${item.title}\n${chalk.gray(`${item.username}/${item.name}`)}`,
Description: truncateDescription(item.description),
'Users (30d)': chalk.cyan(`${item.stats?.totalUsers30Days ?? 0}`),
Pricing: formatPricingModel(item.currentPricingInfo?.pricingModel),
});
}

simpleLog({
message: table.render(CompactMode.WebLikeCompact),
stdout: true,
});
}
}