|
| 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