Skip to content

Commit ada9674

Browse files
patrikbraborecclaudel2ysho
authored
feat: add actors search command to search Apify Store (#1047)
## Summary - Adds `apify actors search` subcommand that searches the Apify Store for Actors matching a query - Supports filtering by category, author username, pricing model, and sort order - Uses `apify-client` SDK (`StoreCollectionClient`) instead of raw `fetch` for consistency with all other commands - Does not require authentication - Supports `--json` output flag ## Test plan - [ ] Run `yarn dev:apify actors search "web scraper"` and verify table output - [ ] Run `yarn dev:apify actors search --category AI --limit 5 --json` and verify JSON output - [ ] Run `yarn dev:apify actors search --username apify --pricing-model FREE` and verify filtering - [ ] Run `yarn dev:apify actors search "nonexistent-query-xyz"` and verify empty result message - [ ] Verify command works without being logged in (no authentication required) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Richard Solár <solar.richard@gmail.com>
1 parent 0e5e3a9 commit ada9674

5 files changed

Lines changed: 423 additions & 13 deletions

File tree

docs/reference.md

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -574,19 +574,20 @@ DESCRIPTION
574574
Manages Actor creation, deployment, and execution on the Apify platform.
575575
576576
SUBCOMMANDS
577-
actors start Starts Actor remotely and returns run details
578-
immediately.
579-
actors rm Permanently removes an Actor from your account.
580-
actors push Deploys Actor to Apify platform using settings from
581-
'.actor/actor.json'.
582-
actors pull Download Actor code to current directory. Clones Git
583-
repositories or fetches Actor files based on the source type.
584-
actors ls Prints a list of recently executed Actors or Actors
585-
you own.
586-
actors info Get information about an Actor.
587-
actors call Executes Actor remotely using your authenticated
588-
account.
589-
actors build Creates a new build of the Actor.
577+
actors start Starts Actor remotely and returns run details
578+
immediately.
579+
actors rm Permanently removes an Actor from your account.
580+
actors search Searches Actors in the Apify Store.
581+
actors push Deploys Actor to Apify platform using settings
582+
from '.actor/actor.json'.
583+
actors pull Download Actor code to current directory. Clones
584+
Git repositories or fetches Actor files based on the source type.
585+
actors ls Prints a list of recently executed Actors or
586+
Actors you own.
587+
actors info Get information about an Actor.
588+
actors call Executes Actor remotely using your authenticated
589+
account.
590+
actors build Creates a new build of the Actor.
590591
```
591592
592593
##### `apify actors ls`
@@ -608,6 +609,41 @@ FLAGS
608609
--offset=<value> Number of Actors that will be skipped.
609610
```
610611
612+
##### `apify actors search`
613+
614+
```sh
615+
DESCRIPTION
616+
Searches Actors in the Apify Store.
617+
618+
Searches the Apify Store for Actors matching the given query. Results can be
619+
filtered by category, author, pricing model, and more. This command does not
620+
require authentication.
621+
622+
USAGE
623+
$ apify actors search [query] [--category <value>]
624+
[--json] [--limit <value>] [--offset <value>]
625+
[--pricing-model <value>] [--sort-by <value>]
626+
[--username <value>]
627+
628+
ARGUMENTS
629+
query Search query to find Actors by title, name, description, username,
630+
or readme.
631+
632+
FLAGS
633+
--category=<value> Filter by category (e.g.
634+
AI).
635+
--json Format the command output as
636+
JSON
637+
--limit=<value> Maximum number of results to
638+
return.
639+
--offset=<value> Number of results to skip
640+
for pagination.
641+
--pricing-model=<value> Filter by pricing model.
642+
--sort-by=<value> Sort order for the results.
643+
--username=<value> Filter by Actor author
644+
username.
645+
```
646+
611647
##### `apify actors rm`
612648
613649
```sh

scripts/generate-cli-docs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const categories: Record<string, CommandsInCategory[]> = {
3939
//
4040
{ command: Commands.actors },
4141
{ command: Commands.actorsLs },
42+
{ command: Commands.actorsSearch },
4243
{ command: Commands.actorsRm },
4344
],
4445
'actor-deploy': [

src/commands/actors/_index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ActorsLsCommand } from './ls.js';
66
import { ActorsPullCommand } from './pull.js';
77
import { ActorsPushCommand } from './push.js';
88
import { ActorsRmCommand } from './rm.js';
9+
import { ActorsSearchCommand } from './search.js';
910
import { ActorsStartCommand } from './start.js';
1011

1112
export class ActorsIndexCommand extends ApifyCommand<typeof ActorsIndexCommand> {
@@ -17,6 +18,7 @@ export class ActorsIndexCommand extends ApifyCommand<typeof ActorsIndexCommand>
1718
//
1819
ActorsStartCommand,
1920
ActorsRmCommand,
21+
ActorsSearchCommand,
2022
ActorsPushCommand,
2123
ActorsPullCommand,
2224
ActorsLsCommand,

src/commands/actors/search.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { ApifyClient } from 'apify-client';
2+
import chalk from 'chalk';
3+
4+
import { ApifyCommand } from '../../lib/command-framework/apify-command.js';
5+
import { Args } from '../../lib/command-framework/args.js';
6+
import { Flags } from '../../lib/command-framework/flags.js';
7+
import { CompactMode, ResponsiveTable } from '../../lib/commands/responsive-table.js';
8+
import { CommandExitCodes } from '../../lib/consts.js';
9+
import { error, info, simpleLog } from '../../lib/outputs.js';
10+
import { getApifyClientOptions, printJsonToStdout } from '../../lib/utils.js';
11+
12+
const pricingModelLabels: Record<string, string> = {
13+
FREE: 'Free',
14+
FLAT_PRICE_PER_MONTH: 'Subscription',
15+
PRICE_PER_DATASET_ITEM: 'Pay per result',
16+
PAY_PER_EVENT: 'Pay per event',
17+
};
18+
19+
function formatPricingModel(model?: string): string {
20+
if (!model) return chalk.gray('Unknown');
21+
22+
return pricingModelLabels[model] ?? model;
23+
}
24+
25+
function truncateDescription(description?: string, maxLength = 60): string {
26+
if (!description) return '';
27+
28+
if (description.length <= maxLength) return description;
29+
30+
return `${description.slice(0, maxLength - 1)}…`;
31+
}
32+
33+
export class ActorsSearchCommand extends ApifyCommand<typeof ActorsSearchCommand> {
34+
static override name = 'search' as const;
35+
36+
static override description =
37+
'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.';
38+
39+
static override args = {
40+
query: Args.string({
41+
description: 'Search query to find Actors by title, name, description, username, or readme.',
42+
required: false,
43+
}),
44+
};
45+
46+
static override flags = {
47+
'sort-by': Flags.string({
48+
description: 'Sort order for the results.',
49+
options: ['relevance', 'popularity', 'newest', 'lastUpdate'],
50+
default: 'relevance',
51+
}),
52+
category: Flags.string({
53+
description: 'Filter by category (e.g. AI).',
54+
}),
55+
username: Flags.string({
56+
description: 'Filter by Actor author username.',
57+
}),
58+
'pricing-model': Flags.string({
59+
description: 'Filter by pricing model.',
60+
options: ['FREE', 'FLAT_PRICE_PER_MONTH', 'PRICE_PER_DATASET_ITEM', 'PAY_PER_EVENT'],
61+
}),
62+
limit: Flags.integer({
63+
description: 'Maximum number of results to return.',
64+
default: 20,
65+
}),
66+
offset: Flags.integer({
67+
description: 'Number of results to skip for pagination.',
68+
default: 0,
69+
}),
70+
};
71+
72+
static override enableJsonFlag = true;
73+
74+
async run() {
75+
const { query } = this.args;
76+
const { json, sortBy, category, username, pricingModel, limit, offset } = this.flags;
77+
78+
const clientOptions = getApifyClientOptions();
79+
delete clientOptions.token;
80+
const client = new ApifyClient(clientOptions);
81+
82+
let result;
83+
84+
try {
85+
result = await client.store().list({
86+
search: query,
87+
sortBy,
88+
category,
89+
username,
90+
pricingModel,
91+
limit,
92+
offset,
93+
});
94+
} catch (err) {
95+
process.exitCode = CommandExitCodes.RunFailed;
96+
error({
97+
message: `Failed to search Apify Store: ${err instanceof Error ? err.message : String(err)}`,
98+
stdout: true,
99+
});
100+
return;
101+
}
102+
103+
if (result.count === 0) {
104+
if (json) {
105+
printJsonToStdout(result);
106+
return;
107+
}
108+
109+
info({ message: 'No Actors found matching your search.', stdout: true });
110+
return;
111+
}
112+
113+
if (json) {
114+
printJsonToStdout(result);
115+
return;
116+
}
117+
118+
const table = new ResponsiveTable({
119+
allColumns: ['Name', 'Description', 'Users (30d)', 'Pricing'],
120+
mandatoryColumns: ['Name', 'Pricing'],
121+
columnAlignments: {
122+
'Users (30d)': 'right',
123+
Name: 'left',
124+
},
125+
});
126+
127+
for (const item of result.items) {
128+
table.pushRow({
129+
Name: `${item.title}\n${chalk.gray(`${item.username}/${item.name}`)}`,
130+
Description: truncateDescription(item.description),
131+
'Users (30d)': chalk.cyan(`${item.stats?.totalUsers30Days ?? 0}`),
132+
Pricing: formatPricingModel(item.currentPricingInfo?.pricingModel),
133+
});
134+
}
135+
136+
simpleLog({
137+
message: table.render(CompactMode.WebLikeCompact),
138+
stdout: true,
139+
});
140+
}
141+
}

0 commit comments

Comments
 (0)