From d01eb4b37f4cc27eb1f24c10162f056279ab1f5e Mon Sep 17 00:00:00 2001 From: patrikbraborec Date: Mon, 13 Apr 2026 16:15:47 +0200 Subject: [PATCH 1/5] feat: improve --help output for AI agents and humans Extends the command framework with examples, docsUrl, interactive, and group metadata, reworks the main help menu with category groups, and adds EXAMPLES + LEARN MORE sections modeled after gh. Also fixes misleading namespace descriptions, clarifies the 'actor' entrypoint's relationship to 'apify actor', supports 'help' as a positional arg equivalent to --help, and normalizes sentence punctuation across all descriptions. Refs #1060 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/actor/_index.ts | 8 +- src/commands/actor/calculate-memory.ts | 15 ++ src/commands/actor/charge.ts | 27 ++- src/commands/actor/generate-schema-types.ts | 19 ++ src/commands/actor/get-input.ts | 11 ++ src/commands/actor/get-public-url.ts | 13 +- src/commands/actor/get-value.ts | 13 +- src/commands/actor/push-data.ts | 23 ++- src/commands/actor/set-value.ts | 26 ++- src/commands/actors/_index.ts | 8 +- src/commands/actors/call.ts | 23 +++ src/commands/actors/info.ts | 17 ++ src/commands/actors/ls.ts | 17 ++ src/commands/actors/pull.ts | 23 ++- src/commands/actors/push.ts | 23 ++- src/commands/actors/rm.ts | 13 ++ src/commands/actors/search.ts | 21 +++ src/commands/actors/start.ts | 17 ++ src/commands/auth/_index.ts | 7 +- src/commands/auth/login.ts | 28 ++- src/commands/auth/logout.ts | 11 ++ src/commands/auth/token.ts | 9 + src/commands/builds/_index.ts | 7 +- src/commands/builds/add-tag.ts | 13 ++ src/commands/builds/create.ts | 13 ++ src/commands/builds/info.ts | 9 + src/commands/builds/log.ts | 9 + src/commands/builds/ls.ts | 13 ++ src/commands/builds/remove-tag.ts | 13 ++ src/commands/builds/rm.ts | 14 ++ src/commands/cli-management/upgrade.ts | 11 ++ src/commands/create.ts | 24 +++ src/commands/datasets/_index.ts | 7 +- src/commands/datasets/create.ts | 13 ++ src/commands/datasets/get-items.ts | 21 ++- src/commands/datasets/info.ts | 9 + src/commands/datasets/ls.ts | 13 ++ src/commands/datasets/push-items.ts | 15 +- src/commands/datasets/rename.ts | 13 ++ src/commands/datasets/rm.ts | 13 ++ src/commands/edit-input-schema.ts | 20 +++ src/commands/info.ts | 11 ++ src/commands/init.ts | 24 +++ src/commands/key-value-stores/_index.ts | 7 +- src/commands/key-value-stores/create.ts | 13 ++ src/commands/key-value-stores/delete-value.ts | 13 ++ src/commands/key-value-stores/get-value.ts | 13 ++ src/commands/key-value-stores/info.ts | 9 + src/commands/key-value-stores/keys.ts | 13 ++ src/commands/key-value-stores/ls.ts | 13 ++ src/commands/key-value-stores/rename.ts | 13 ++ src/commands/key-value-stores/rm.ts | 13 ++ src/commands/key-value-stores/set-value.ts | 13 ++ src/commands/request-queues/_index.ts | 7 +- src/commands/run.ts | 23 +++ src/commands/runs/_index.ts | 8 +- src/commands/runs/abort.ts | 13 ++ src/commands/runs/info.ts | 17 ++ src/commands/runs/log.ts | 9 + src/commands/runs/ls.ts | 17 ++ src/commands/runs/resurrect.ts | 9 + src/commands/runs/rm.ts | 13 ++ src/commands/secrets/_index.ts | 28 +-- src/commands/secrets/add.ts | 13 +- src/commands/secrets/ls.ts | 9 + src/commands/secrets/rm.ts | 11 +- src/commands/task/_index.ts | 7 +- src/commands/task/run.ts | 13 ++ src/commands/telemetry/_index.ts | 6 +- src/commands/telemetry/disable.ts | 9 + src/commands/telemetry/enable.ts | 9 + src/commands/validate-schema.ts | 15 ++ src/entrypoints/_shared.ts | 21 ++- src/lib/command-framework/apify-command.ts | 23 +++ src/lib/command-framework/help.ts | 170 ++++++++++-------- src/lib/command-framework/help/CommandHelp.ts | 14 ++ .../help/CommandWithSubcommands.ts | 12 ++ .../help/_BaseCommandRenderer.ts | 57 +++++- 78 files changed, 1185 insertions(+), 125 deletions(-) diff --git a/src/commands/actor/_index.ts b/src/commands/actor/_index.ts index ecfa66841..24fbaaef9 100644 --- a/src/commands/actor/_index.ts +++ b/src/commands/actor/_index.ts @@ -11,7 +11,13 @@ import { ActorSetValueCommand } from './set-value.js'; export class ActorIndexCommand extends ApifyCommand { static override name = 'actor' as const; - static override description = 'Manages runtime data operations inside of a running Actor.'; + static override description = + `Runtime data operations intended to be called from inside a running Actor: read input, push data, get/set records in the default key-value store, charge pay-per-event, generate schema types.\n\n` + + `For platform-level management (deploy, list, call Actors), see 'apify actors' (plural).`; + + static override group = 'Apify Console'; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-actor'; static override subcommands = [ // diff --git a/src/commands/actor/calculate-memory.ts b/src/commands/actor/calculate-memory.ts index 230ec8031..e2fc2c62f 100644 --- a/src/commands/actor/calculate-memory.ts +++ b/src/commands/actor/calculate-memory.ts @@ -31,6 +31,21 @@ export class ActorCalculateMemoryCommand extends ApifyCommand static override description = 'Charge for a specific event in the pay-per-event Actor run.'; + static override group = 'Actor Runtime'; + + static override examples = [ + { + description: 'Charge one event of the given type.', + command: 'actor charge result-item', + }, + { + description: 'Charge 5 events with an idempotency key.', + command: 'actor charge result-item --count 5 --idempotency-key req-123', + }, + { + description: 'Test locally without actually charging.', + command: 'actor charge result-item --test-pay-per-event', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#actor-charge'; + static override args = { eventName: Args.string({ - description: 'Name of the event to charge for', + description: 'Name of the event to charge for.', required: true, }), }; static override flags = { 'count': Flags.integer({ - description: 'Number of events to charge', + description: 'Number of events to charge.', required: false, default: 1, }), 'idempotency-key': Flags.string({ - description: 'Idempotency key for the charge request', + description: 'Idempotency key for the charge request.', required: false, }), 'test-pay-per-event': Flags.boolean({ - description: 'Test pay-per-event charging without actually charging', + description: 'Test pay-per-event charging without actually charging.', required: false, default: false, }), diff --git a/src/commands/actor/generate-schema-types.ts b/src/commands/actor/generate-schema-types.ts index 6450a0b66..961637639 100644 --- a/src/commands/actor/generate-schema-types.ts +++ b/src/commands/actor/generate-schema-types.ts @@ -56,6 +56,25 @@ Reads the input schema from one of these locations (in priority order): Optionally specify custom schema path to use.`; + static override group = 'Actor Runtime'; + + static override examples = [ + { + description: 'Generate TypeScript types from the input schema into the default output directory.', + command: 'actor generate-schema-types', + }, + { + description: 'Generate types from a custom schema path.', + command: 'actor generate-schema-types ./schemas/my-input.json', + }, + { + description: 'Mark all generated properties as optional.', + command: 'actor generate-schema-types --all-optional', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#actor-generate-schema-types'; + static override flags = { output: Flags.string({ char: 'o', diff --git a/src/commands/actor/get-input.ts b/src/commands/actor/get-input.ts index d5205eff0..adfa77ebd 100644 --- a/src/commands/actor/get-input.ts +++ b/src/commands/actor/get-input.ts @@ -7,6 +7,17 @@ export class ActorGetInputCommand extends ApifyCommand { static override name = 'push-data' as const; - static override description = - "Saves data to Actor's run default dataset.\n\n" + - 'Accept input as:\n' + - ' - JSON argument:\n' + - ' $ apify actor push-data {"key": "value"}\n' + - ' - Piped stdin:\n' + - ' $ cat ./test.json | apify actor push-data'; + static override description = "Saves data to Actor's run default dataset."; + + static override group = 'Actor Runtime'; + + static override examples = [ + { + description: 'Push a single item as an inline JSON argument.', + command: `actor push-data '{"key":"value"}'`, + }, + { + description: 'Push an array of items by piping from stdin.', + command: 'cat ./items.json | actor push-data', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#actor-push-data'; static override args = { item: Args.string({ diff --git a/src/commands/actor/set-value.ts b/src/commands/actor/set-value.ts index c70dc5af8..4e35c8805 100644 --- a/src/commands/actor/set-value.ts +++ b/src/commands/actor/set-value.ts @@ -7,12 +7,26 @@ export class ActorSetValueCommand extends ApifyCommand { static override name = 'actors' as const; - static override description = 'Manages Actor creation, deployment, and execution on the Apify platform.'; + static override description = + `Search, list, deploy, and call Actors on the Apify platform.\n` + + `For runtime operations inside a running Actor (push-data, get-input, set-value…), see 'apify actor' (singular).`; + + static override group = 'Apify Console'; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-actors'; static override subcommands = [ // diff --git a/src/commands/actors/call.ts b/src/commands/actors/call.ts index c1bf38b1e..55746a40f 100644 --- a/src/commands/actors/call.ts +++ b/src/commands/actors/call.ts @@ -32,6 +32,29 @@ export class ActorsCallCommand extends ApifyCommand { 'Executes Actor remotely using your authenticated account.\n' + 'Reads input from local key-value store by default.'; + static override group = 'Apify Console'; + + static override examples = [ + { + description: 'Call the Actor defined in the current directory (from .actor/actor.json).', + command: 'apify call', + }, + { + description: 'Call a specific Actor by its full name.', + command: 'apify call apify/hello-world', + }, + { + description: 'Call an Actor with inline JSON input.', + command: `apify call apify/hello-world --input '{"url":"https://example.com"}'`, + }, + { + description: 'Call an Actor with input from a file and print the dataset on success.', + command: 'apify call apify/web-scraper --input-file input.json --output-dataset', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-call'; + static override flags = { ...SharedRunOnCloudFlags('Actor'), input: Flags.string({ diff --git a/src/commands/actors/info.ts b/src/commands/actors/info.ts index b3c3fccf0..91faaf9b6 100644 --- a/src/commands/actors/info.ts +++ b/src/commands/actors/info.ts @@ -46,6 +46,23 @@ export class ActorsInfoCommand extends ApifyCommand { static override description = 'Get information about an Actor.'; + static override examples = [ + { + description: 'Print summary information about an Actor.', + command: 'apify actors info apify/hello-world', + }, + { + description: 'Print the Actor README only.', + command: 'apify actors info apify/hello-world --readme', + }, + { + description: 'Print the Actor input schema as JSON.', + command: 'apify actors info apify/hello-world --input', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-actors-info'; + static override flags = { readme: Flags.boolean({ description: 'Return the Actor README.', diff --git a/src/commands/actors/ls.ts b/src/commands/actors/ls.ts index 7b7f6b406..54cb0e493 100644 --- a/src/commands/actors/ls.ts +++ b/src/commands/actors/ls.ts @@ -99,6 +99,23 @@ export class ActorsLsCommand extends ApifyCommand { static override description = 'Prints a list of recently executed Actors or Actors you own.'; + static override examples = [ + { + description: 'List Actors you recently interacted with.', + command: 'apify actors ls', + }, + { + description: 'List Actors you own, newest first.', + command: 'apify actors ls --my --desc', + }, + { + description: 'List the next page of 50 Actors.', + command: 'apify actors ls --limit 50 --offset 50', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-actors-ls'; + static override flags = { my: Flags.boolean({ description: 'Whether to list Actors made by the logged in user.', diff --git a/src/commands/actors/pull.ts b/src/commands/actors/pull.ts index 7513d1d6a..c09cf1057 100644 --- a/src/commands/actors/pull.ts +++ b/src/commands/actors/pull.ts @@ -31,14 +31,33 @@ export class ActorsPullCommand extends ApifyCommand { 'Download Actor code to current directory. ' + 'Clones Git repositories or fetches Actor files based on the source type.'; + static override group = 'Local Actor Development'; + + static override examples = [ + { + description: 'Pull the Actor linked to the current directory from the Apify platform.', + command: 'apify pull', + }, + { + description: 'Pull a specific Actor by its full name into a target directory.', + command: 'apify pull apify/hello-world --dir ./hello-world', + }, + { + description: 'Pull a specific version of an Actor.', + command: 'apify pull apify/hello-world --version 1.2', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-pull'; + static override flags = { version: Flags.string({ char: 'v', - description: 'Actor version number which will be pulled, e.g. 1.2. Default: the highest version', + description: 'Actor version number which will be pulled, e.g. 1.2. Default: the highest version.', required: false, }), dir: Flags.string({ - description: 'Directory where the Actor should be pulled to', + description: 'Directory where the Actor should be pulled to.', required: false, }), }; diff --git a/src/commands/actors/push.ts b/src/commands/actors/push.ts index 6e515f359..79345d52f 100644 --- a/src/commands/actors/push.ts +++ b/src/commands/actors/push.ts @@ -52,6 +52,25 @@ export class ActorsPushCommand extends ApifyCommand { `Use negation patterns (e.g. !dist/) in .actorignore to force-include git-ignored files.\n` + `Use --force to override newer remote versions.`; + static override group = 'Local Actor Development'; + + static override examples = [ + { + description: 'Deploy the current Actor to the Apify platform.', + command: 'apify push', + }, + { + description: 'Deploy to a specific Actor by ID, overriding newer remote versions.', + command: 'apify push E2jjCZBezvAZnX8Rb --force', + }, + { + description: 'Deploy without waiting for the build to finish.', + command: 'apify push --no-wait-for-finish', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-push'; + static override enableJsonFlag = true; static override flags = { @@ -62,7 +81,7 @@ export class ActorsPushCommand extends ApifyCommand { }), 'build-tag': Flags.string({ char: 'b', - description: `Build tag to be applied to the successful Actor build. By default, it is taken from the '${LOCAL_CONFIG_PATH}' file`, + description: `Build tag to be applied to the successful Actor build. By default, it is taken from the '${LOCAL_CONFIG_PATH}' file.`, required: false, }), 'wait-for-finish': Flags.string({ @@ -82,7 +101,7 @@ export class ActorsPushCommand extends ApifyCommand { required: false, }), dir: Flags.string({ - description: 'Directory where the Actor is located', + description: 'Directory where the Actor is located.', required: false, }), 'allow-missing-secrets': Flags.boolean({ diff --git a/src/commands/actors/rm.ts b/src/commands/actors/rm.ts index 168b863ca..b353ed4f6 100644 --- a/src/commands/actors/rm.ts +++ b/src/commands/actors/rm.ts @@ -11,6 +11,19 @@ export class ActorsRmCommand extends ApifyCommand { static override description = 'Permanently removes an Actor from your account.'; + static override interactive = true; + + static override interactiveNote = 'Prompts for confirmation before deleting. Cannot be bypassed; deletion is irreversible.'; + + static override examples = [ + { + description: 'Delete an Actor by its full name (prompts for confirmation).', + command: 'apify actors rm my-username/my-actor', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-actors-rm'; + static override args = { actorId: Args.string({ description: 'The Actor ID to delete.', diff --git a/src/commands/actors/search.ts b/src/commands/actors/search.ts index 4fde24e2b..d1a576e18 100644 --- a/src/commands/actors/search.ts +++ b/src/commands/actors/search.ts @@ -36,6 +36,27 @@ export class ActorsSearchCommand extends ApifyCommand 'Starts Actor remotely and returns run details immediately.\n' + 'Uses authenticated account and local key-value store for input.'; + static override examples = [ + { + description: 'Start the Actor defined in the current directory and return immediately.', + command: 'apify actors start', + }, + { + description: 'Start a specific Actor with inline JSON input.', + command: `apify actors start apify/hello-world --input '{"url":"https://example.com"}'`, + }, + { + description: 'Start with input from a file and custom memory/timeout.', + command: 'apify actors start apify/web-scraper --input-file ./input.json --memory 4096 --timeout 600', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-actors-start'; + static override flags = { ...SharedRunOnCloudFlags('Actor'), input: Flags.string({ diff --git a/src/commands/auth/_index.ts b/src/commands/auth/_index.ts index b81984261..454916ce7 100644 --- a/src/commands/auth/_index.ts +++ b/src/commands/auth/_index.ts @@ -6,7 +6,12 @@ import { AuthTokenCommand } from './token.js'; export class AuthIndexCommand extends ApifyCommand { static override name = 'auth' as const; - static override description = 'Manages authentication for Apify CLI.'; + static override description = + 'Log in, log out, and inspect your stored Apify API token. Convenience aliases are also exposed at the top level as `apify login` / `apify logout`.'; + + static override group = 'Authentication'; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-auth'; static override subcommands = [AuthLoginCommand, AuthLogoutCommand, AuthTokenCommand]; diff --git a/src/commands/auth/login.ts b/src/commands/auth/login.ts index 90bee3c99..06e2b7100 100644 --- a/src/commands/auth/login.ts +++ b/src/commands/auth/login.ts @@ -53,15 +53,39 @@ export class AuthLoginCommand extends ApifyCommand { `All other commands use these stored credentials.\n\n` + `Run 'apify logout' to remove authentication.`; + static override group = 'Authentication'; + + static override interactive = true; + + static override interactiveNote = + 'Prompts for an API token if not provided. To run non-interactively, pass --token .'; + + static override examples = [ + { + description: 'Log in interactively (opens a browser to complete the flow).', + command: 'apify login', + }, + { + description: 'Log in non-interactively with an API token.', + command: 'apify login --token apify_api_xxxxx', + }, + { + description: 'Log in by manually pasting a token instead of opening a browser.', + command: 'apify login --method manual', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-login'; + static override flags = { token: Flags.string({ char: 't', - description: 'Apify API token', + description: 'Apify API token.', required: false, }), method: Flags.string({ char: 'm', - description: 'Method of logging in to Apify', + description: 'Method of logging in to Apify.', choices: ['console', 'manual'], required: false, }), diff --git a/src/commands/auth/logout.ts b/src/commands/auth/logout.ts index c8b3b47df..a324e9878 100644 --- a/src/commands/auth/logout.ts +++ b/src/commands/auth/logout.ts @@ -12,6 +12,17 @@ export class AuthLogoutCommand extends ApifyCommand { `Removes authentication by deleting your API token and account information from '${tildify(AUTH_FILE_PATH())}'.\n` + `Run 'apify login' to authenticate again.`; + static override group = 'Authentication'; + + static override examples = [ + { + description: 'Remove the stored Apify credentials.', + command: 'apify logout', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-logout'; + async run() { await rimrafPromised(AUTH_FILE_PATH()); diff --git a/src/commands/auth/token.ts b/src/commands/auth/token.ts index 1abb0eca4..c2a307e88 100644 --- a/src/commands/auth/token.ts +++ b/src/commands/auth/token.ts @@ -7,6 +7,15 @@ export class AuthTokenCommand extends ApifyCommand { static override description = 'Prints the current API token for the Apify CLI.'; + static override examples = [ + { + description: 'Print the stored API token to stdout (use with care — it is a secret).', + command: 'apify auth token', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-auth-token'; + async run() { await getLoggedClientOrThrow(); const userInfo = await getLocalUserInfo(); diff --git a/src/commands/builds/_index.ts b/src/commands/builds/_index.ts index 936397bb4..ccddfa1f2 100644 --- a/src/commands/builds/_index.ts +++ b/src/commands/builds/_index.ts @@ -10,7 +10,12 @@ import { BuildsRmCommand } from './rm.js'; export class BuildsIndexCommand extends ApifyCommand { static override name = 'builds' as const; - static override description = 'Manages Actor build processes and versioning.'; + static override description = + 'Inspect, tag, and delete Actor builds on the Apify platform. Also supports starting a new build.'; + + static override group = 'Apify Console'; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-builds'; static override subcommands = [ // diff --git a/src/commands/builds/add-tag.ts b/src/commands/builds/add-tag.ts index 1472f97c4..a450be39f 100644 --- a/src/commands/builds/add-tag.ts +++ b/src/commands/builds/add-tag.ts @@ -11,6 +11,19 @@ export class BuildsAddTagCommand extends ApifyCommand --tag latest', + }, + { + description: 'Tag a build with a custom name like "beta".', + command: 'apify builds add-tag --build --tag beta', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-builds-add-tag'; + static override flags = { build: Flags.string({ char: 'b', diff --git a/src/commands/builds/create.ts b/src/commands/builds/create.ts index 58c69d10f..19dc2968d 100644 --- a/src/commands/builds/create.ts +++ b/src/commands/builds/create.ts @@ -18,6 +18,19 @@ export class BuildsCreateCommand extends ApifyCommand { static override description = 'Prints information about a specific build.'; + static override examples = [ + { + description: 'Print information about a build.', + command: 'apify builds info ', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-builds-info'; + static override args = { buildId: Args.string({ required: true, diff --git a/src/commands/builds/log.ts b/src/commands/builds/log.ts index 4b2a0c1d9..686f7eda9 100644 --- a/src/commands/builds/log.ts +++ b/src/commands/builds/log.ts @@ -8,6 +8,15 @@ export class BuildsLogCommand extends ApifyCommand { static override description = 'Prints the log of a specific build.'; + static override examples = [ + { + description: 'Print the build log.', + command: 'apify builds log ', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-builds-log'; + static override args = { buildId: Args.string({ required: true, diff --git a/src/commands/builds/ls.ts b/src/commands/builds/ls.ts index f4ab0954b..db7f9c1cc 100644 --- a/src/commands/builds/ls.ts +++ b/src/commands/builds/ls.ts @@ -24,6 +24,19 @@ export class BuildsLsCommand extends ApifyCommand { static override description = 'Lists all builds of the Actor.'; + static override examples = [ + { + description: 'List builds of the Actor in the current directory.', + command: 'apify builds ls', + }, + { + description: 'List builds of a specific Actor.', + command: 'apify builds ls apify/hello-world', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-builds-ls'; + static override flags = { offset: Flags.integer({ description: 'Number of builds that will be skipped.', diff --git a/src/commands/builds/remove-tag.ts b/src/commands/builds/remove-tag.ts index 6ca252e8e..774d37509 100644 --- a/src/commands/builds/remove-tag.ts +++ b/src/commands/builds/remove-tag.ts @@ -12,6 +12,19 @@ export class BuildsRemoveTagCommand extends ApifyCommand --tag beta', + }, + { + description: 'Remove a tag non-interactively.', + command: 'apify builds remove-tag --build --tag beta --yes', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-builds-remove-tag'; + static override flags = { build: Flags.string({ char: 'b', diff --git a/src/commands/builds/rm.ts b/src/commands/builds/rm.ts index 5c87fee58..e60af854d 100644 --- a/src/commands/builds/rm.ts +++ b/src/commands/builds/rm.ts @@ -12,6 +12,20 @@ export class BuildsRmCommand extends ApifyCommand { static override description = 'Permanently removes an Actor build from the Apify platform.'; + static override interactive = true; + + static override interactiveNote = + 'Prompts for confirmation before deleting. Tagged builds additionally require typing the tag name.'; + + static override examples = [ + { + description: 'Delete a build (prompts for confirmation).', + command: 'apify builds rm ', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-builds-rm'; + static override args = { buildId: Args.string({ description: 'The build ID to delete.', diff --git a/src/commands/cli-management/upgrade.ts b/src/commands/cli-management/upgrade.ts index fe318158c..7ade40737 100644 --- a/src/commands/cli-management/upgrade.ts +++ b/src/commands/cli-management/upgrade.ts @@ -41,6 +41,17 @@ export class UpgradeCommand extends ApifyCommand { static override description = 'Checks that installed Apify CLI version is up to date.'; + static override group = 'Utilities'; + + static override examples = [ + { + description: 'Check for a newer CLI version and upgrade if available.', + command: 'apify upgrade', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-upgrade'; + static override hidden = true; static override aliases = ['cv', 'check-version']; diff --git a/src/commands/create.ts b/src/commands/create.ts index 524cc5187..6b60358ee 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -44,6 +44,30 @@ export class CreateCommand extends ApifyCommand { static override description = 'Creates an Actor project from a template in a new directory. The command automatically initializes a git repository in the newly created Actor directory.'; + static override group = 'Local Actor Development'; + + static override interactive = true; + + static override interactiveNote = + 'Prompts for an Actor name and template if not provided. To run non-interactively, pass the name as a positional argument and --template.'; + + static override examples = [ + { + description: 'Create a new Actor project interactively (prompts for name and template).', + command: 'apify create', + }, + { + description: 'Create non-interactively with explicit name and template.', + command: 'apify create my-actor --template js-crawlee-cheerio', + }, + { + description: 'Create without installing dependencies (faster; run install yourself later).', + command: 'apify create my-actor --template python-start --skip-dependency-install', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-create'; + static override flags = { template: Flags.string({ char: 't', diff --git a/src/commands/datasets/_index.ts b/src/commands/datasets/_index.ts index 330ea21e1..d5b2abea5 100644 --- a/src/commands/datasets/_index.ts +++ b/src/commands/datasets/_index.ts @@ -10,7 +10,12 @@ import { DatasetsRmCommand } from './rm.js'; export class DatasetsIndexCommand extends ApifyCommand { static override name = 'datasets' as const; - static override description = 'Manages structured data storage and retrieval.'; + static override description = + 'Manage Apify datasets — create, list, rename, delete, push items, and download items in various formats.'; + + static override group = 'Apify Console'; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-datasets'; static override subcommands = [ DatasetsCreateCommand, diff --git a/src/commands/datasets/create.ts b/src/commands/datasets/create.ts index 03435debe..9dc6a8397 100644 --- a/src/commands/datasets/create.ts +++ b/src/commands/datasets/create.ts @@ -11,6 +11,19 @@ export class DatasetsCreateCommand extends ApifyCommand { static override description = 'Retrieves dataset items in specified format (JSON, CSV, etc).'; + static override examples = [ + { + description: 'Print all items from a dataset as JSON.', + command: 'apify datasets get-items ', + }, + { + description: 'Export the first 100 items as CSV to a file.', + command: 'apify datasets get-items --format csv --limit 100 > items.csv', + }, + { + description: 'Paginate: skip the first 500 items, return the next 500.', + command: 'apify datasets get-items --offset 500 --limit 500', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-datasets-get-items'; + static override flags = { limit: Flags.integer({ description: @@ -30,7 +47,7 @@ export class DatasetsGetItems extends ApifyCommand { description: 'The offset in the dataset where to start getting items.', }), format: Flags.string({ - description: "The format of the returned output. By default, it is set to 'json'", + description: "The format of the returned output. By default, it is set to 'json'.", choices: Object.keys(downloadFormatToContentType) as DownloadItemsFormat[], default: DownloadItemsFormat.JSON, }), @@ -38,7 +55,7 @@ export class DatasetsGetItems extends ApifyCommand { static override args = { datasetId: Args.string({ - description: 'The ID of the Dataset to export the items for', + description: 'The ID of the Dataset to export the items for.', required: true, }), }; diff --git a/src/commands/datasets/info.ts b/src/commands/datasets/info.ts index 38d88f4b4..846141f54 100644 --- a/src/commands/datasets/info.ts +++ b/src/commands/datasets/info.ts @@ -20,6 +20,15 @@ export class DatasetsInfoCommand extends ApifyCommand', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-datasets-info'; + static override args = { storeId: Args.string({ description: 'The dataset store ID to print information about.', diff --git a/src/commands/datasets/ls.ts b/src/commands/datasets/ls.ts index 901fae6f5..2a6102b39 100644 --- a/src/commands/datasets/ls.ts +++ b/src/commands/datasets/ls.ts @@ -20,6 +20,19 @@ export class DatasetsLsCommand extends ApifyCommand { static override description = 'Prints all datasets on your account.'; + static override examples = [ + { + description: 'List your datasets (most recently modified first).', + command: 'apify datasets ls --desc', + }, + { + description: 'List only unnamed (auto-generated) datasets.', + command: 'apify datasets ls --unnamed', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-datasets-ls'; + static override flags = { offset: Flags.integer({ description: 'Number of datasets that will be skipped.', diff --git a/src/commands/datasets/push-items.ts b/src/commands/datasets/push-items.ts index 2593e71ce..dff4d30c8 100644 --- a/src/commands/datasets/push-items.ts +++ b/src/commands/datasets/push-items.ts @@ -13,10 +13,23 @@ export class DatasetsPushDataCommand extends ApifyCommand { static override description = 'Permanently removes a dataset.'; + static override interactive = true; + + static override interactiveNote = 'Prompts for confirmation before deleting.'; + + static override examples = [ + { + description: 'Delete a dataset by name or ID (prompts for confirmation).', + command: 'apify datasets rm my-dataset', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-datasets-rm'; + static override args = { datasetNameOrId: Args.string({ description: 'The dataset ID or name to delete', diff --git a/src/commands/edit-input-schema.ts b/src/commands/edit-input-schema.ts index 533ed1bf9..06e8a63f4 100644 --- a/src/commands/edit-input-schema.ts +++ b/src/commands/edit-input-schema.ts @@ -28,6 +28,26 @@ export class EditInputSchemaCommand extends ApifyCommand { static override description = 'Prints details about your currently authenticated Apify account.'; + static override group = 'Apify Console'; + + static override examples = [ + { + description: 'Print the currently logged-in account username and user ID.', + command: 'apify info', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-info'; + async run() { await getLoggedClientOrThrow(); const info = await getLocalUserInfo(); diff --git a/src/commands/init.ts b/src/commands/init.ts index 347d97f50..03248e1f6 100644 --- a/src/commands/init.ts +++ b/src/commands/init.ts @@ -23,6 +23,30 @@ export class InitCommand extends ApifyCommand { `Creates the '${LOCAL_CONFIG_PATH}' file and the '${DEFAULT_LOCAL_STORAGE_DIR}' directory in the current directory, but does not touch any other existing files or directories.\n\n` + `WARNING: Overwrites existing '${DEFAULT_LOCAL_STORAGE_DIR}' directory.`; + static override group = 'Local Actor Development'; + + static override interactive = true; + + static override interactiveNote = + 'Prompts for an Actor name if not provided. To run non-interactively, pass the Actor name as a positional argument, or pass --yes to accept the default (current directory name).'; + + static override examples = [ + { + description: 'Initialize an Actor in the current directory, prompting for a name.', + command: 'apify init', + }, + { + description: 'Initialize non-interactively with an explicit Actor name.', + command: 'apify init my-actor', + }, + { + description: 'Initialize non-interactively, accepting the default Actor name.', + command: 'apify init --yes', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-init'; + static override args = { actorName: Args.string({ required: false, diff --git a/src/commands/key-value-stores/_index.ts b/src/commands/key-value-stores/_index.ts index bf22eb2c4..83c679c43 100644 --- a/src/commands/key-value-stores/_index.ts +++ b/src/commands/key-value-stores/_index.ts @@ -12,7 +12,12 @@ import { KeyValueStoresSetValueCommand } from './set-value.js'; export class KeyValueStoresIndexCommand extends ApifyCommand { static override name = 'key-value-stores' as const; - static override description = 'Manages persistent key-value storage.\n\nAlias: kvs'; + static override description = + 'Manage Apify key-value stores — create, list, rename, delete stores, and get/set/delete individual records.\n\nAvailable as alias: kvs.'; + + static override group = 'Apify Console'; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-key-value-stores'; static override hiddenAliases = ['kvs']; diff --git a/src/commands/key-value-stores/create.ts b/src/commands/key-value-stores/create.ts index 91f4b2dd4..7a9d1e402 100644 --- a/src/commands/key-value-stores/create.ts +++ b/src/commands/key-value-stores/create.ts @@ -11,6 +11,19 @@ export class KeyValueStoresCreateCommand extends ApifyCommand OUTPUT', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-key-value-stores-delete-value'; + static override args = { 'store id': Args.string({ description: 'The key-value store ID to delete the value from.', diff --git a/src/commands/key-value-stores/get-value.ts b/src/commands/key-value-stores/get-value.ts index 3ed746c32..bef95284e 100644 --- a/src/commands/key-value-stores/get-value.ts +++ b/src/commands/key-value-stores/get-value.ts @@ -11,6 +11,19 @@ export class KeyValueStoresGetValueCommand extends ApifyCommand INPUT', + }, + { + description: 'Print only the record content-type.', + command: 'apify key-value-stores get-value INPUT --only-content-type', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-key-value-stores-get-value'; + static override flags = { 'only-content-type': Flags.boolean({ description: 'Only return the content type of the specified key', diff --git a/src/commands/key-value-stores/info.ts b/src/commands/key-value-stores/info.ts index c5c2611dd..3e1b4f77f 100644 --- a/src/commands/key-value-stores/info.ts +++ b/src/commands/key-value-stores/info.ts @@ -20,6 +20,15 @@ export class KeyValueStoresInfoCommand extends ApifyCommand', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-key-value-stores-info'; + static override args = { storeId: Args.string({ description: 'The key-value store ID to print information about.', diff --git a/src/commands/key-value-stores/keys.ts b/src/commands/key-value-stores/keys.ts index 569886da0..f21b296c6 100644 --- a/src/commands/key-value-stores/keys.ts +++ b/src/commands/key-value-stores/keys.ts @@ -17,6 +17,19 @@ export class KeyValueStoresKeysCommand extends ApifyCommand', + }, + { + description: 'Paginate keys past an exclusive start key.', + command: 'apify key-value-stores keys --exclusive-start-key last-seen-key --limit 100', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-key-value-stores-keys'; + static override flags = { limit: Flags.integer({ description: 'The maximum number of keys to return.', diff --git a/src/commands/key-value-stores/ls.ts b/src/commands/key-value-stores/ls.ts index a9cda3f57..880d595af 100644 --- a/src/commands/key-value-stores/ls.ts +++ b/src/commands/key-value-stores/ls.ts @@ -17,6 +17,19 @@ export class KeyValueStoresLsCommand extends ApifyCommand OUTPUT '{"status":"done"}'`, + }, + { + description: 'Store a plain-text file read from stdin.', + command: 'cat ./report.txt | apify key-value-stores set-value REPORT --content-type text/plain', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-key-value-stores-set-value'; + static override flags = { 'content-type': Flags.string({ description: 'The MIME content type of the value. By default, "application/json" is assumed.', diff --git a/src/commands/request-queues/_index.ts b/src/commands/request-queues/_index.ts index 36a9c5752..ba4573ee9 100644 --- a/src/commands/request-queues/_index.ts +++ b/src/commands/request-queues/_index.ts @@ -3,7 +3,12 @@ import { ApifyCommand } from '../../lib/command-framework/apify-command.js'; export class RequestQueuesIndexCommand extends ApifyCommand { static override name = 'request-queues' as const; - static override description = 'Manages URL queues for web scraping and automation tasks.'; + static override description = + 'Manage Apify request queues. No subcommands are available yet — reserved namespace for future additions.'; + + static override group = 'Apify Console'; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-request-queues'; async run() { this.printHelp(); diff --git a/src/commands/run.ts b/src/commands/run.ts index 917f9593f..1db39671e 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -56,6 +56,29 @@ export class RunCommand extends ApifyCommand { `Stores data in local '${DEFAULT_LOCAL_STORAGE_DIR}' directory.\n\n` + `NOTE: For Node.js Actors, customize behavior by modifying the 'start' script in package.json file.`; + static override group = 'Local Actor Development'; + + static override examples = [ + { + description: 'Run the Actor in the current directory with the stored input.', + command: 'apify run', + }, + { + description: 'Run and purge the default storage first (dataset, request queue, key-value store).', + command: 'apify run --purge', + }, + { + description: 'Run with inline JSON input (overrides the stored INPUT).', + command: `apify run --input '{"startUrls":[{"url":"https://example.com"}]}'`, + }, + { + description: 'Run with input from a file.', + command: 'apify run --input-file ./input.json', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-run'; + static override flags = { purge: Flags.boolean({ char: 'p', diff --git a/src/commands/runs/_index.ts b/src/commands/runs/_index.ts index a2d13327c..685a951cc 100644 --- a/src/commands/runs/_index.ts +++ b/src/commands/runs/_index.ts @@ -9,7 +9,13 @@ import { RunsRmCommand } from './rm.js'; export class RunsIndexCommand extends ApifyCommand { static override name = 'runs' as const; - static override description = 'Manages Actor run operations '; + static override description = + `Inspect, abort, resurrect, or delete existing Actor runs.\n` + + `Does not start new runs — use 'apify call' (synchronous) or 'apify actors start' (asynchronous) for that.`; + + static override group = 'Apify Console'; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-runs'; static override subcommands = [ RunsAbortCommand, diff --git a/src/commands/runs/abort.ts b/src/commands/runs/abort.ts index cb2e5dc10..669d0cc6d 100644 --- a/src/commands/runs/abort.ts +++ b/src/commands/runs/abort.ts @@ -17,6 +17,19 @@ export class RunsAbortCommand extends ApifyCommand { static override description = 'Aborts an Actor run.'; + static override examples = [ + { + description: 'Abort a running Actor gracefully (up to 30s drain).', + command: 'apify runs abort ', + }, + { + description: 'Force-abort a running Actor immediately.', + command: 'apify runs abort --force', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-runs-abort'; + static override args = { runId: Args.string({ required: true, diff --git a/src/commands/runs/info.ts b/src/commands/runs/info.ts index fcf813d4a..96000fb6a 100644 --- a/src/commands/runs/info.ts +++ b/src/commands/runs/info.ts @@ -44,6 +44,23 @@ export class RunsInfoCommand extends ApifyCommand { static override description = 'Prints information about an Actor run.'; + static override examples = [ + { + description: 'Show a summary of a run.', + command: 'apify runs info ', + }, + { + description: 'Show verbose details including usage breakdown.', + command: 'apify runs info --verbose', + }, + { + description: 'Emit the full run object as JSON.', + command: 'apify runs info --json', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-runs-info'; + static override args = { runId: Args.string({ required: true, diff --git a/src/commands/runs/log.ts b/src/commands/runs/log.ts index a59a0432b..cc8fcfe24 100644 --- a/src/commands/runs/log.ts +++ b/src/commands/runs/log.ts @@ -8,6 +8,15 @@ export class RunsLogCommand extends ApifyCommand { static override description = 'Prints the log of a specific run.'; + static override examples = [ + { + description: 'Print the log of a specific run to stdout.', + command: 'apify runs log ', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-runs-log'; + static override args = { runId: Args.string({ required: true, diff --git a/src/commands/runs/ls.ts b/src/commands/runs/ls.ts index a531da255..e4808979d 100644 --- a/src/commands/runs/ls.ts +++ b/src/commands/runs/ls.ts @@ -30,6 +30,23 @@ export class RunsLsCommand extends ApifyCommand { static override description = 'Lists all runs of the Actor.'; + static override examples = [ + { + description: 'List runs of the Actor in the current directory.', + command: 'apify runs ls', + }, + { + description: 'List runs of a specific Actor.', + command: 'apify runs ls apify/hello-world', + }, + { + description: 'List the 50 most recent runs in descending order.', + command: 'apify runs ls --limit 50 --desc', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-runs-ls'; + static override flags = { offset: Flags.integer({ description: 'Number of runs that will be skipped.', diff --git a/src/commands/runs/resurrect.ts b/src/commands/runs/resurrect.ts index 0dbdcb05e..a41c87300 100644 --- a/src/commands/runs/resurrect.ts +++ b/src/commands/runs/resurrect.ts @@ -19,6 +19,15 @@ export class RunsResurrectCommand extends ApifyCommand', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-runs-resurrect'; + static override args = { runId: Args.string({ required: true, diff --git a/src/commands/runs/rm.ts b/src/commands/runs/rm.ts index a3c6b5d71..83a8b8e3b 100644 --- a/src/commands/runs/rm.ts +++ b/src/commands/runs/rm.ts @@ -20,6 +20,19 @@ export class RunsRmCommand extends ApifyCommand { static override description = 'Deletes an Actor Run.'; + static override interactive = true; + + static override interactiveNote = 'Prompts for confirmation before deleting.'; + + static override examples = [ + { + description: 'Delete a finished or aborted run (prompts for confirmation).', + command: 'apify runs rm ', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-runs-rm'; + static override args = { runId: Args.string({ description: 'The run ID to delete.', diff --git a/src/commands/secrets/_index.ts b/src/commands/secrets/_index.ts index b52d0eca8..6e687b665 100644 --- a/src/commands/secrets/_index.ts +++ b/src/commands/secrets/_index.ts @@ -8,17 +8,23 @@ export class SecretsIndexCommand extends ApifyCommand { static override description = `Adds a new secret to '~/.apify' for use in Actor environment variables.`; + static override examples = [ + { + description: 'Add a secret named "mySecret". Reference it in .actor/actor.json as "@mySecret".', + command: 'apify secrets add mySecret TopSecretValue123', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-secrets-add'; + static override args = { name: Args.string({ required: true, - description: 'Name of the secret', + description: 'Name of the secret.', }), value: Args.string({ required: true, - description: 'Value of the secret', + description: 'Value of the secret.', }), }; diff --git a/src/commands/secrets/ls.ts b/src/commands/secrets/ls.ts index c867bebc9..76b5c1f97 100644 --- a/src/commands/secrets/ls.ts +++ b/src/commands/secrets/ls.ts @@ -19,6 +19,15 @@ export class SecretsLsCommand extends ApifyCommand { static override description = 'Lists all secret keys stored in your local configuration.'; + static override examples = [ + { + description: 'List the names of all locally stored secrets.', + command: 'apify secrets ls', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-secrets-ls'; + static override enableJsonFlag = true; async run() { diff --git a/src/commands/secrets/rm.ts b/src/commands/secrets/rm.ts index 92b35802a..f3e57163a 100644 --- a/src/commands/secrets/rm.ts +++ b/src/commands/secrets/rm.ts @@ -7,10 +7,19 @@ export class SecretsRmCommand extends ApifyCommand { static override description = 'Permanently deletes a secret from your stored credentials.'; + static override examples = [ + { + description: 'Delete a locally stored secret by name.', + command: 'apify secrets rm mySecret', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-secrets-rm'; + static override args = { name: Args.string({ required: true, - description: 'Name of the secret', + description: 'Name of the secret.', }), }; diff --git a/src/commands/task/_index.ts b/src/commands/task/_index.ts index 7a5c711bd..d8d1ecf64 100644 --- a/src/commands/task/_index.ts +++ b/src/commands/task/_index.ts @@ -4,7 +4,12 @@ import { TaskRunCommand } from './run.js'; export class TasksIndexCommand extends ApifyCommand { static override name = 'task' as const; - static override description = 'Manages scheduled and predefined Actor configurations.'; + static override description = + `Run saved Apify tasks (named Actor configurations). Currently supports 'task run' only; create and manage tasks in the Apify Console.`; + + static override group = 'Apify Console'; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-task'; static override subcommands = [TaskRunCommand]; diff --git a/src/commands/task/run.ts b/src/commands/task/run.ts index 905006404..5f3acdad4 100644 --- a/src/commands/task/run.ts +++ b/src/commands/task/run.ts @@ -14,6 +14,19 @@ export class TaskRunCommand extends ApifyCommand { 'Executes predefined Actor task remotely using local key-value store for input.\n' + 'Customize with --memory and --timeout flags.\n'; + static override examples = [ + { + description: 'Run a task by name.', + command: 'apify task run my-task', + }, + { + description: 'Run a task by full ID with custom memory and timeout.', + command: 'apify task run username/my-task --memory 4096 --timeout 600', + }, + ]; + + static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-task-run'; + static override flags = SharedRunOnCloudFlags('Task'); static override args = { diff --git a/src/commands/telemetry/_index.ts b/src/commands/telemetry/_index.ts index 9ad232f77..caca9ebe6 100644 --- a/src/commands/telemetry/_index.ts +++ b/src/commands/telemetry/_index.ts @@ -6,7 +6,11 @@ export class TelemetryIndexCommand extends ApifyCommand help`, ` help`, and ` help ` as equivalents to `--help`. + // Rewrite these by dropping the `help` positional and setting the --help flag. We skip index 0 so the + // actual `help` command (e.g. `apify help runs`) is left alone. + const helpPositionalIndex = startingResult.positionals.findIndex( + (p, i) => i > 0 && p.toLowerCase() === 'help', + ); + if (helpPositionalIndex !== -1) { + const helpPositional = startingResult.positionals[helpPositionalIndex]; + const argvIndex = startingArgs.indexOf(helpPositional); + if (argvIndex !== -1) startingArgs.splice(argvIndex, 1); + if (!startingArgs.includes('--help') && !startingArgs.includes('-h')) { + startingArgs.push('--help'); + } + startingResult.positionals.splice(helpPositionalIndex, 1); + startingResult.values.help = true; + } + + const maybeSubcommandName = startingResult.positionals[1]; let hasSubcommand = false; const baseCommand = commandRegistry.get(commandName); diff --git a/src/lib/command-framework/apify-command.ts b/src/lib/command-framework/apify-command.ts index fc32080b5..3a0094455 100644 --- a/src/lib/command-framework/apify-command.ts +++ b/src/lib/command-framework/apify-command.ts @@ -183,6 +183,29 @@ export abstract class ApifyCommand'.`, + '', + ); + } else { + result.push( + 'Apify command-line interface (CLI) helps you manage the Apify cloud platform and develop, build, and deploy Apify Actors.', + '', + ); + } result.push(chalk.bold('VERSION')); result.push(` ${cliMetadata.fullVersionString}`); @@ -78,80 +109,79 @@ export function renderMainHelpMenu(entrypoint: string) { result.push(` $ ${entrypoint} [options]`); result.push(''); - const allGroupedByType = mapGroupBy(commands, ([command, helpGenerator]) => { - // We register the help generators for all subcommands with an entrypoint that includes the root command, - // so we need to ignore those - if (helpGenerator.entrypoint.includes(' ')) { - return 'ignored'; - } - - if (command.hidden) { - return 'ignored'; - } - - if (helpGenerator instanceof CommandWithSubcommandsHelp) { - return 'subcommand'; - } - - return 'command'; - }); - - const groupedSubcommands = allGroupedByType.get('subcommand')?.sort(sortByName); - const groupedCommands = allGroupedByType.get('command')?.sort(sortByName); - - if (groupedSubcommands?.length) { - result.push(chalk.bold('TOPICS')); - - const lines: string[] = []; - - const widestTopicNameLength = widestLine(groupedSubcommands.map(([subcommand]) => subcommand.name).join('\n')); - - for (const [subcommand] of groupedSubcommands) { - if (subcommand.hidden) { - continue; - } - - const shortDescription = subcommand.shortDescription || subcommand.description?.split('\n')[0] || ''; - - const fullString = `${subcommand.name.padEnd(widestTopicNameLength)} ${shortDescription}`; - - const wrapped = wrapAnsi(fullString, getMaxLineWidth() - widestTopicNameLength - 2); - - lines.push(` ${indentString(wrapped, widestTopicNameLength + 2 + 2).trim()}`); - } - - result.push(...lines, ''); + // Collect top-level (non-hidden, no-parent) commands — leaves and namespaces alike share groups. + const topLevelCommands: [typeof BuiltApifyCommand, BaseCommandRenderer][] = []; + for (const [command, helpGenerator] of commands) { + // Subcommand help generators have a space in their entrypoint ("apify runs"); skip those. + if (helpGenerator.entrypoint.includes(' ')) continue; + if (command.hidden) continue; + topLevelCommands.push([command, helpGenerator]); } - if (groupedCommands?.length) { - result.push(chalk.bold('COMMANDS')); - - const lines: string[] = []; - - const widestCommandNameLength = widestLine(groupedCommands.map(([command]) => command.name).join('\n')); - - for (const [command] of groupedCommands) { - if (command.hidden) { - continue; - } - - const shortDescription = command.shortDescription || command.description?.split('\n')[0] || ''; - - const fullString = `${command.name.padEnd(widestCommandNameLength)} ${shortDescription}`; + // Group commands by their static `group` field (falling back to OTHER_GROUP) + const byGroup = new Map(); + for (const entry of topLevelCommands) { + const [command] = entry; + const group = command.group || OTHER_GROUP; + if (!byGroup.has(group)) byGroup.set(group, []); + byGroup.get(group)!.push(entry); + } - const wrapped = wrapAnsi(fullString, getMaxLineWidth() - widestCommandNameLength - 2); + // Sort group contents alphabetically + for (const entries of byGroup.values()) { + entries.sort(sortByName); + } - lines.push(` ${indentString(wrapped, widestCommandNameLength + 2 + 2).trim()}`); - } + // Render groups in canonical order, then any remaining groups (alphabetical), then OTHER last + const orderedGroupNames: string[] = []; + for (const g of GROUP_ORDER) { + if (byGroup.has(g)) orderedGroupNames.push(g); + } + const extraGroups = [...byGroup.keys()] + .filter((g) => !GROUP_ORDER.includes(g as (typeof GROUP_ORDER)[number]) && g !== OTHER_GROUP) + .sort(); + orderedGroupNames.push(...extraGroups); + if (byGroup.has(OTHER_GROUP)) orderedGroupNames.push(OTHER_GROUP); + + // Compute widest command name so description columns align across sections. + const allNames = topLevelCommands.map(([c]) => c.name).join('\n'); + const widestNameLength = widestLine(allNames) || 1; + + const renderEntry = ([command]: [typeof BuiltApifyCommand, BaseCommandRenderer]): string => { + const shortDescription = command.shortDescription || command.description?.split('\n')[0] || ''; + const label = command.name.padEnd(widestNameLength); + const fullString = `${label} ${shortDescription}`; + // -4 = 2 leading spaces on the line + 2 spaces between label and description + const wrapped = wrapAnsi(fullString, getMaxLineWidth() - widestNameLength - 4); + // Indent so continuation lines line up under the description column (label + 2-space separator + 2 leading) + return ` ${indentString(wrapped, widestNameLength + 2 + 2).trim()}`; + }; + + for (const groupName of orderedGroupNames) { + const entries = byGroup.get(groupName)!; + if (!entries.length) continue; + + result.push(chalk.bold(groupName.toUpperCase())); + result.push(...entries.map(renderEntry), ''); + } - result.push(...lines, ''); + const examples = entrypoint === 'actor' ? ACTOR_EXAMPLES : APIFY_EXAMPLES; + if (examples.length) { + result.push(chalk.bold('EXAMPLES')); + result.push(...examples.map((ex) => ` $ ${ex}`), ''); } result.push( - chalk.bold('TROUBLESHOOTING'), - ' For general support, reach out to us at https://apify.com/contact', + chalk.bold('LEARN MORE'), + ` Use '${entrypoint} --help' for more information about a command.`, + ` Read the docs at https://docs.apify.com/cli.`, '', - ' If you believe you are encountering a bug, file it at https://github.com/apify/apify-cli/issues/new', + ); + + result.push( + chalk.bold('TROUBLESHOOTING'), + ' For general support, reach out to us at https://apify.com/contact.', + ' If you believe you are encountering a bug, file it at https://github.com/apify/apify-cli/issues/new.', ); return result.join('\n').trim(); diff --git a/src/lib/command-framework/help/CommandHelp.ts b/src/lib/command-framework/help/CommandHelp.ts index 8601a4a31..8f14114ba 100644 --- a/src/lib/command-framework/help/CommandHelp.ts +++ b/src/lib/command-framework/help/CommandHelp.ts @@ -56,6 +56,12 @@ export class CommandHelp extends BaseCommandRenderer { this.pushDescription(result); } + this.pushExamples(result); + + this.pushInteractiveNote(result); + + this.pushLearnMore(result); + return result.join('\n').trim(); } @@ -74,6 +80,14 @@ export class CommandHelp extends BaseCommandRenderer { this.pushDescription(result); } + if (options.showExamples) { + this.pushExamples(result); + } + + if (options.showLearnMore) { + this.pushLearnMore(result); + } + return result.join('\n').trim(); } diff --git a/src/lib/command-framework/help/CommandWithSubcommands.ts b/src/lib/command-framework/help/CommandWithSubcommands.ts index 821e8216b..fbfee6ae8 100644 --- a/src/lib/command-framework/help/CommandWithSubcommands.ts +++ b/src/lib/command-framework/help/CommandWithSubcommands.ts @@ -48,6 +48,10 @@ export class CommandWithSubcommandsHelp extends BaseCommandRenderer { this.pushSubcommands(result); + this.pushExamples(result); + + this.pushLearnMore(result); + return result.join('\n').trim(); } @@ -66,6 +70,14 @@ export class CommandWithSubcommandsHelp extends BaseCommandRenderer { this.pushSubcommands(result); } + if (options.showExamples) { + this.pushExamples(result); + } + + if (options.showLearnMore) { + this.pushLearnMore(result); + } + return result.join('\n').trim(); } diff --git a/src/lib/command-framework/help/_BaseCommandRenderer.ts b/src/lib/command-framework/help/_BaseCommandRenderer.ts index cfcc49a99..452d30c1c 100644 --- a/src/lib/command-framework/help/_BaseCommandRenderer.ts +++ b/src/lib/command-framework/help/_BaseCommandRenderer.ts @@ -12,6 +12,8 @@ export interface SelectiveRenderOptions { showDescription?: boolean; showUsageString?: boolean; showSubcommands?: boolean; + showExamples?: boolean; + showLearnMore?: boolean; } export abstract class BaseCommandRenderer { @@ -29,11 +31,13 @@ export abstract class BaseCommandRenderer { public abstract selectiveRender(options: SelectiveRenderOptions): string; protected pushShortDescription(result: string[]) { + const interactiveLabel = this.command.interactive ? `${chalk.yellow('[INTERACTIVE]')} ` : ''; + if (this.command.shortDescription) { - result.push(this.command.shortDescription, ''); + result.push(`${interactiveLabel}${this.command.shortDescription}`, ''); // Fallback to first line of description } else if (this.command.description) { - result.push(this.command.description.split('\n')[0], ''); + result.push(`${interactiveLabel}${this.command.description.split('\n')[0]}`, ''); } } @@ -52,6 +56,55 @@ export abstract class BaseCommandRenderer { result.push(''); } + protected pushExamples(result: string[]) { + const examples = this.command.examples; + + if (!examples?.length) { + return; + } + + result.push(chalk.bold('EXAMPLES')); + + for (const example of examples) { + if (example.description) { + const wrapped = wrap(example.description, getMaxLineWidth() - 2, { trim: false }); + const indented = indent(wrapped, 2); + result.push(chalk.dim(indented)); + } + + result.push(` $ ${example.command}`); + result.push(''); + } + } + + protected pushInteractiveNote(result: string[]) { + if (!this.command.interactive) { + return; + } + + result.push(chalk.bold('NOTE')); + + const defaultNote = + 'This command prompts the user for input. To run non-interactively (e.g. in CI or from an AI agent), pass all required arguments and flags explicitly.'; + const note = this.command.interactiveNote || defaultNote; + + const wrapped = wrap(note, getMaxLineWidth() - 2, { trim: false }); + const indented = indent(wrapped, 2); + + result.push(indented); + result.push(''); + } + + protected pushLearnMore(result: string[]) { + if (!this.command.docsUrl) { + return; + } + + result.push(chalk.bold('LEARN MORE')); + result.push(` ${this.command.docsUrl}`); + result.push(''); + } + protected pushNewLineBeforeNewEntryIfLengthIsPastTheLimit({ state, itemToAdd, From 56b8addde044e7ec65cf543364d122e0d1f7e71c Mon Sep 17 00:00:00 2001 From: patrikbraborec Date: Tue, 14 Apr 2026 14:13:23 +0200 Subject: [PATCH 2/5] fix: resolve lint and format issues in help output changes Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/actors/rm.ts | 3 ++- src/commands/secrets/_index.ts | 3 +-- src/entrypoints/_shared.ts | 4 +--- src/lib/command-framework/help/_BaseCommandRenderer.ts | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/commands/actors/rm.ts b/src/commands/actors/rm.ts index b353ed4f6..265aca5ca 100644 --- a/src/commands/actors/rm.ts +++ b/src/commands/actors/rm.ts @@ -13,7 +13,8 @@ export class ActorsRmCommand extends ApifyCommand { static override interactive = true; - static override interactiveNote = 'Prompts for confirmation before deleting. Cannot be bypassed; deletion is irreversible.'; + static override interactiveNote = + 'Prompts for confirmation before deleting. Cannot be bypassed; deletion is irreversible.'; static override examples = [ { diff --git a/src/commands/secrets/_index.ts b/src/commands/secrets/_index.ts index 6e687b665..69b1146da 100644 --- a/src/commands/secrets/_index.ts +++ b/src/commands/secrets/_index.ts @@ -18,8 +18,7 @@ export class SecretsIndexCommand extends ApifyCommand help`, ` help`, and ` help ` as equivalents to `--help`. // Rewrite these by dropping the `help` positional and setting the --help flag. We skip index 0 so the // actual `help` command (e.g. `apify help runs`) is left alone. - const helpPositionalIndex = startingResult.positionals.findIndex( - (p, i) => i > 0 && p.toLowerCase() === 'help', - ); + const helpPositionalIndex = startingResult.positionals.findIndex((p, i) => i > 0 && p.toLowerCase() === 'help'); if (helpPositionalIndex !== -1) { const helpPositional = startingResult.positionals[helpPositionalIndex]; const argvIndex = startingArgs.indexOf(helpPositional); diff --git a/src/lib/command-framework/help/_BaseCommandRenderer.ts b/src/lib/command-framework/help/_BaseCommandRenderer.ts index 452d30c1c..b59976fd8 100644 --- a/src/lib/command-framework/help/_BaseCommandRenderer.ts +++ b/src/lib/command-framework/help/_BaseCommandRenderer.ts @@ -57,7 +57,7 @@ export abstract class BaseCommandRenderer { } protected pushExamples(result: string[]) { - const examples = this.command.examples; + const { examples } = this.command; if (!examples?.length) { return; From 8762e3b02f2b433c599f83b9fb0a530b1afa683f Mon Sep 17 00:00:00 2001 From: patrikbraborec Date: Wed, 15 Apr 2026 10:22:26 +0200 Subject: [PATCH 3/5] feat: render help examples as shell-style # comments Per-command EXAMPLES and the main help menu now prefix example descriptions with "# " (shell-comment style), matching the `gh` CLI convention. Each example becomes a self-contained copy-paste block, which prevents AI agents from mistaking description prose for part of the command. Also updates the main-help `apify create` example to its interactive form (dropping the stray `my-actor` positional) with a comment explaining the flow, so users don't copy an invocation that looks incomplete. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/command-framework/help.ts | 49 ++++++++++++++----- .../help/_BaseCommandRenderer.ts | 12 ++++- 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/lib/command-framework/help.ts b/src/lib/command-framework/help.ts index 888b2a661..848d690d8 100644 --- a/src/lib/command-framework/help.ts +++ b/src/lib/command-framework/help.ts @@ -69,20 +69,26 @@ const OTHER_GROUP = 'Other'; /** * Canonical example invocations shown at the bottom of the main help screen. - * Kept short and representative of typical user flows. + * Kept short and representative of typical user flows. Descriptions render as + * shell-style `#` comments above the command (matches the per-command EXAMPLES + * rendering) and should be used to flag interactive flows whose bare form might + * otherwise look incomplete. */ -const APIFY_EXAMPLES = [ - 'apify login', - 'apify create my-actor', - 'apify run', - 'apify push', - 'apify actors search "web scraper"', +const APIFY_EXAMPLES: { description?: string; command: string }[] = [ + { command: 'apify login' }, + { + description: 'Walks you interactively through the Actor creation flow.', + command: 'apify create', + }, + { command: 'apify run' }, + { command: 'apify push' }, + { command: 'apify actors search "web scraper"' }, ]; -const ACTOR_EXAMPLES = [ - 'actor get-input', - `actor push-data '{"url":"https://example.com"}'`, - 'actor set-value OUTPUT \'{"done":true}\'', +const ACTOR_EXAMPLES: { description?: string; command: string }[] = [ + { command: 'actor get-input' }, + { command: `actor push-data '{"url":"https://example.com"}'` }, + { command: `actor set-value OUTPUT '{"done":true}'` }, ]; export function renderMainHelpMenu(entrypoint: string) { @@ -168,7 +174,26 @@ export function renderMainHelpMenu(entrypoint: string) { const examples = entrypoint === 'actor' ? ACTOR_EXAMPLES : APIFY_EXAMPLES; if (examples.length) { result.push(chalk.bold('EXAMPLES')); - result.push(...examples.map((ex) => ` $ ${ex}`), ''); + for (let i = 0; i < examples.length; i++) { + const ex = examples[i]; + // Commented entries stand as their own block: blank line before (unless first) + // and after (unless last) so the `#` comment clearly pairs with its command + // and doesn't visually bleed into neighbouring bare examples. + if (ex.description) { + if (i > 0) result.push(''); + // -4 leaves room for the 2-space indent plus the "# " prefix so + // continuation lines remain valid shell comments at any terminal width. + const wrapped = wrapAnsi(ex.description, getMaxLineWidth() - 4, { trim: false }); + const commented = wrapped + .split('\n') + .map((line) => `# ${line}`) + .join('\n'); + result.push(chalk.dim(indentString(commented, 2))); + } + result.push(` $ ${ex.command}`); + if (ex.description && i < examples.length - 1) result.push(''); + } + result.push(''); } result.push( diff --git a/src/lib/command-framework/help/_BaseCommandRenderer.ts b/src/lib/command-framework/help/_BaseCommandRenderer.ts index b59976fd8..e92a1e470 100644 --- a/src/lib/command-framework/help/_BaseCommandRenderer.ts +++ b/src/lib/command-framework/help/_BaseCommandRenderer.ts @@ -67,8 +67,16 @@ export abstract class BaseCommandRenderer { for (const example of examples) { if (example.description) { - const wrapped = wrap(example.description, getMaxLineWidth() - 2, { trim: false }); - const indented = indent(wrapped, 2); + // Render descriptions as shell-style `#` comments so each example is a + // self-contained copy-paste block (matches the `gh` CLI convention and + // prevents description prose from being mistaken for part of the command). + // -4 leaves room for the 2-space indent plus the "# " prefix. + const wrapped = wrap(example.description, getMaxLineWidth() - 4, { trim: false }); + const commented = wrapped + .split('\n') + .map((line) => `# ${line}`) + .join('\n'); + const indented = indent(commented, 2); result.push(chalk.dim(indented)); } From 7807c2c9173f8d2396210b1c7601094cb78fb942 Mon Sep 17 00:00:00 2001 From: patrikbraborec Date: Wed, 15 Apr 2026 14:20:47 +0200 Subject: [PATCH 4/5] fix: prevent help from hijacking commands with positional args The previous help-rewrite logic scanned every positional past index 0 for the literal word "help", which hijacked commands whose own args happened to be "help" (e.g. `actor set-value key1 help` or `actor set-value help value1` would show help instead of writing to the key-value store). Only treat a positional as a help trigger when it is an actual subcommand slot: position 1 when the base command has subcommands, and position 2 only when position 1 is a real subcommand and that subcommand takes no positional args of its own. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/entrypoints/_shared.ts | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/entrypoints/_shared.ts b/src/entrypoints/_shared.ts index b7216cc5e..7671072dc 100644 --- a/src/entrypoints/_shared.ts +++ b/src/entrypoints/_shared.ts @@ -150,10 +150,24 @@ export async function runCLI(entrypoint: string) { const commandName = startingResult.positionals[0]; - // Be defensive: support ` help`, ` help`, and ` help ` as equivalents to `--help`. - // Rewrite these by dropping the `help` positional and setting the --help flag. We skip index 0 so the - // actual `help` command (e.g. `apify help runs`) is left alone. - const helpPositionalIndex = startingResult.positionals.findIndex((p, i) => i > 0 && p.toLowerCase() === 'help'); + // Be defensive: support ` help`, ` help `, and ` help` as equivalents to `--help`. + // We must not hijack user-provided positional args that happen to be the word "help". A position is only + // a "help slot" if it is a subcommand slot for that command — i.e., the command at that level has + // subcommands. If the command has its own positional args instead, `help` at that position is a real value + // (e.g. `actor set-value help value1` must pass `help` as the key). + let helpPositionalIndex = -1; + const baseCommandForHelpCheck = commandRegistry.get(commandName); + if (baseCommandForHelpCheck?.subcommands?.length) { + if (startingResult.positionals[1]?.toLowerCase() === 'help') { + helpPositionalIndex = 1; + } else if (startingResult.positionals[2]?.toLowerCase() === 'help') { + const subcommand = commandRegistry.get(`${commandName} ${startingResult.positionals[1]}`); + if (subcommand && !subcommand.args) { + helpPositionalIndex = 2; + } + } + } + if (helpPositionalIndex !== -1) { const helpPositional = startingResult.positionals[helpPositionalIndex]; const argvIndex = startingArgs.indexOf(helpPositional); From 21c4ff740eb49b61bdcc11a03244300900a2b8fa Mon Sep 17 00:00:00 2001 From: patrikbraborec Date: Wed, 15 Apr 2026 15:03:23 +0200 Subject: [PATCH 5/5] refactor: address staff review for help output improvements Implements the findings from the staff review of the help output rework: prefix bare 'actor' example commands with the current entrypoint so they stay copy-pasteable when viewed under 'apify actor', render the interactive note on namespace commands, drop the now-unused mapGroupBy helper, add unit tests for the help renderers, and note the no-op path of the help-positional rewrite under the actor entrypoint. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/entrypoints/_shared.ts | 4 + .../help/CommandWithSubcommands.ts | 2 + .../help/_BaseCommandRenderer.ts | 27 +- src/lib/utils.ts | 21 -- test/local/lib/command-framework/help.test.ts | 237 ++++++++++++++++++ 5 files changed, 269 insertions(+), 22 deletions(-) create mode 100644 test/local/lib/command-framework/help.test.ts diff --git a/src/entrypoints/_shared.ts b/src/entrypoints/_shared.ts index 7671072dc..501469d7d 100644 --- a/src/entrypoints/_shared.ts +++ b/src/entrypoints/_shared.ts @@ -155,6 +155,10 @@ export async function runCLI(entrypoint: string) { // a "help slot" if it is a subcommand slot for that command — i.e., the command at that level has // subcommands. If the command has its own positional args instead, `help` at that position is a real value // (e.g. `actor set-value help value1` must pass `help` as the key). + // + // Note: in the standalone `actor` entrypoint all registered commands (see `actorCommands` in + // `_register.ts`) are leaf commands with no `subcommands`, so the guard below is always falsy + // and this rewrite is effectively a no-op there. It only fires under the `apify` entrypoint. let helpPositionalIndex = -1; const baseCommandForHelpCheck = commandRegistry.get(commandName); if (baseCommandForHelpCheck?.subcommands?.length) { diff --git a/src/lib/command-framework/help/CommandWithSubcommands.ts b/src/lib/command-framework/help/CommandWithSubcommands.ts index fbfee6ae8..2db0ebabc 100644 --- a/src/lib/command-framework/help/CommandWithSubcommands.ts +++ b/src/lib/command-framework/help/CommandWithSubcommands.ts @@ -50,6 +50,8 @@ export class CommandWithSubcommandsHelp extends BaseCommandRenderer { this.pushExamples(result); + this.pushInteractiveNote(result); + this.pushLearnMore(result); return result.join('\n').trim(); diff --git a/src/lib/command-framework/help/_BaseCommandRenderer.ts b/src/lib/command-framework/help/_BaseCommandRenderer.ts index e92a1e470..5efc74cc9 100644 --- a/src/lib/command-framework/help/_BaseCommandRenderer.ts +++ b/src/lib/command-framework/help/_BaseCommandRenderer.ts @@ -80,11 +80,36 @@ export abstract class BaseCommandRenderer { result.push(chalk.dim(indented)); } - result.push(` $ ${example.command}`); + result.push(` $ ${this.normalizeExampleCommand(example.command)}`); result.push(''); } } + /** + * Examples authored on a command may use a short invocation prefix (e.g. "actor push-data") + * that matches the standalone runtime entrypoint. When that same command is reached via a + * longer entrypoint (e.g. "apify actor") the bare form is no longer copy-pasteable. Prepend + * the parent portion of the entrypoint so the example matches the current context. Also + * handles piped invocations like "cat ./items.json | actor push-data". + */ + protected normalizeExampleCommand(command: string): string { + const parts = this.entrypoint.split(' '); + if (parts.length < 2) return command; + + const lastWord = parts[parts.length - 1]; + const parentPrefix = `${parts.slice(0, -1).join(' ')} `; + const escapedLastWord = lastWord.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + + let result = command; + // Prepend the parent prefix when the command begins with the bare entrypoint tail. + if (result.startsWith(`${lastWord} `)) { + result = `${parentPrefix}${result}`; + } + // Also rewrite any piped invocations (e.g. "... | actor foo") to use the full entrypoint. + result = result.replace(new RegExp(`\\|\\s+${escapedLastWord}\\s`, 'g'), `| ${parentPrefix}${lastWord} `); + return result; + } + protected pushInteractiveNote(result: string[]) { if (!this.command.interactive) { return; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 432b1126a..181924457 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -754,27 +754,6 @@ export function objectGroupBy( return result; } -/** - * A "polyfill" for Map.groupBy - */ -export function mapGroupBy(items: Iterable, keySelector: (item: T, index: number) => K): Map { - const map = new Map(); - let index = 0; - - for (const value of items) { - const key = keySelector(value, index++); - const list = map.get(key); - - if (list) { - list.push(value); - } else { - map.set(key, [value]); - } - } - - return map; -} - export function printJsonToStdout(object: unknown) { console.log(JSON.stringify(object, null, 2)); } diff --git a/test/local/lib/command-framework/help.test.ts b/test/local/lib/command-framework/help.test.ts new file mode 100644 index 000000000..5b604bf01 --- /dev/null +++ b/test/local/lib/command-framework/help.test.ts @@ -0,0 +1,237 @@ +/* eslint-disable max-classes-per-file */ +import stripAnsi from 'strip-ansi'; + +import { + ApifyCommand, + type BuiltApifyCommand as _BuiltApifyCommand, + commandRegistry, +} from '../../../../src/lib/command-framework/apify-command.js'; +import { + registerCommandForHelpGeneration, + renderHelpForCommand, + renderMainHelpMenu, +} from '../../../../src/lib/command-framework/help.js'; + +const BuiltApifyCommand = ApifyCommand as typeof _BuiltApifyCommand; + +// Pin the wrap width so rendered output is deterministic across environments. +process.env.APIFY_CLI_MAX_LINE_WIDTH = '200'; + +class FakeRunsAbort extends BuiltApifyCommand { + static override name = 'abort' as const; + static override description = 'Aborts an Actor run.'; + static override examples = [ + { + description: 'Abort a running Actor gracefully.', + command: 'apify runs abort ', + }, + ]; + + static override docsUrl = 'https://example.com/runs-abort'; +} + +class FakeRunsIndex extends BuiltApifyCommand { + static override name = 'runs' as const; + static override description = 'Commands for managing Actor runs.'; + static override group = 'Apify Console'; + static override subcommands = [FakeRunsAbort]; + static override examples = [{ command: 'apify runs ls' }]; +} + +class FakeCreate extends BuiltApifyCommand { + static override name = 'create' as const; + static override description = 'Creates a new Actor project.'; + static override group = 'Local Actor Development'; + static override interactive = true; + static override interactiveNote = 'Pass --template to skip the prompt.'; + static override examples = [{ command: 'apify create my-actor' }]; + static override docsUrl = 'https://example.com/create'; +} + +class FakeActorPushData extends BuiltApifyCommand { + static override name = 'push-data' as const; + static override description = "Saves data to the Actor's default dataset."; + static override examples = [ + { + description: 'Push a single item.', + command: `actor push-data '{"key":"value"}'`, + }, + { + description: 'Push an array via stdin.', + command: 'cat ./items.json | actor push-data', + }, + ]; +} + +class FakeUtility extends BuiltApifyCommand { + static override name = 'utility' as const; + static override description = 'A utility command.'; + // No group → should fall into OTHER. +} + +class FakeInteractiveNamespace extends BuiltApifyCommand { + static override name = 'login-wizard' as const; + static override description = 'Commands for logging in interactively.'; + static override interactive = true; + static override interactiveNote = 'Pass --token to skip prompts.'; + static override subcommands = [ + class extends BuiltApifyCommand { + static override name = 'start' as const; + static override description = 'Starts the wizard.'; + }, + ]; +} + +describe('Help rendering', () => { + let existingCommands: [string, typeof _BuiltApifyCommand][]; + + beforeAll(() => { + existingCommands = [...commandRegistry.entries()]; + commandRegistry.clear(); + + // Register apify-side fakes. + registerCommandForHelpGeneration('apify', FakeCreate); + registerCommandForHelpGeneration('apify', FakeRunsIndex); + registerCommandForHelpGeneration('apify', FakeUtility); + registerCommandForHelpGeneration('apify', FakeInteractiveNamespace); + + // The `actor push-data` subcommand is registered twice with different entrypoints, + // once as a subcommand of `apify actor` and once as a standalone `actor` command. + // Only the second registration survives in the `commands` map, so test each case + // separately below by re-registering with the desired entrypoint. + }); + + afterAll(() => { + commandRegistry.clear(); + for (const [name, command] of existingCommands) { + commandRegistry.set(name, command); + } + }); + + describe('CommandHelp.render()', () => { + test('renders USAGE, DESCRIPTION, EXAMPLES, and LEARN MORE sections', () => { + registerCommandForHelpGeneration('apify', FakeCreate); + const output = stripAnsi(renderHelpForCommand(FakeCreate)); + + expect(output).toContain('USAGE'); + expect(output).toContain('$ apify create'); + expect(output).toContain('DESCRIPTION'); + expect(output).toContain('Creates a new Actor project.'); + expect(output).toContain('EXAMPLES'); + expect(output).toContain('$ apify create my-actor'); + expect(output).toContain('LEARN MORE'); + expect(output).toContain('https://example.com/create'); + }); + + test('renders the interactive note for interactive commands', () => { + registerCommandForHelpGeneration('apify', FakeCreate); + const output = stripAnsi(renderHelpForCommand(FakeCreate)); + + expect(output).toContain('[INTERACTIVE]'); + expect(output).toContain('NOTE'); + expect(output).toContain('Pass --template to skip the prompt.'); + }); + + test('renders example descriptions as shell-style # comments', () => { + registerCommandForHelpGeneration('apify', FakeRunsAbort); + const output = stripAnsi(renderHelpForCommand(FakeRunsAbort)); + + expect(output).toContain('# Abort a running Actor gracefully.'); + expect(output).toContain('$ apify runs abort '); + }); + }); + + describe('CommandWithSubcommandsHelp.render()', () => { + test('renders SUBCOMMANDS section with each child command', () => { + registerCommandForHelpGeneration('apify', FakeRunsIndex); + const output = stripAnsi(renderHelpForCommand(FakeRunsIndex)); + + expect(output).toContain('SUBCOMMANDS'); + expect(output).toContain('runs abort'); + expect(output).toContain('Aborts an Actor run.'); + }); + + test('renders interactive note for interactive namespace commands', () => { + registerCommandForHelpGeneration('apify', FakeInteractiveNamespace); + const output = stripAnsi(renderHelpForCommand(FakeInteractiveNamespace)); + + expect(output).toContain('[INTERACTIVE]'); + expect(output).toContain('NOTE'); + expect(output).toContain('Pass --token to skip prompts.'); + }); + }); + + describe('Example command normalization by entrypoint', () => { + class FakeActor extends BuiltApifyCommand { + static override name = 'actor' as const; + static override description = 'Actor runtime commands.'; + static override subcommands = [FakeActorPushData]; + } + + test('prepends apify to bare "actor" examples when viewed under apify entrypoint', () => { + // Registering under 'apify' re-registers FakeActorPushData as a subcommand with entrypoint "apify actor". + registerCommandForHelpGeneration('apify', FakeActor); + + const output = stripAnsi(renderHelpForCommand(FakeActorPushData)); + + // Leading bare "actor" form gets "apify " prepended. + expect(output).toContain(`$ apify actor push-data '{"key":"value"}'`); + // Piped "| actor" form is also rewritten to "| apify actor". + expect(output).toContain('cat ./items.json | apify actor push-data'); + // The original bare forms should no longer appear standalone. + expect(output).not.toMatch(/\$ actor push-data '\{"key":"value"\}'/); + }); + + test('leaves bare "actor" examples untouched when viewed under actor entrypoint', () => { + // Re-register the same command directly under "actor". + registerCommandForHelpGeneration('actor', FakeActorPushData); + const output = stripAnsi(renderHelpForCommand(FakeActorPushData)); + + expect(output).toContain(`$ actor push-data '{"key":"value"}'`); + expect(output).toContain('cat ./items.json | actor push-data'); + expect(output).not.toContain('apify actor push-data'); + }); + }); + + describe('renderMainHelpMenu()', () => { + test('renders groups in canonical order with OTHER last', () => { + // Re-register all fake commands so the main menu sees them. + registerCommandForHelpGeneration('apify', FakeCreate); + registerCommandForHelpGeneration('apify', FakeRunsIndex); + registerCommandForHelpGeneration('apify', FakeUtility); + + const output = stripAnsi(renderMainHelpMenu('apify')); + + const devIdx = output.indexOf('LOCAL ACTOR DEVELOPMENT'); + const consoleIdx = output.indexOf('APIFY CONSOLE'); + const otherIdx = output.indexOf('OTHER'); + + expect(devIdx).toBeGreaterThan(-1); + expect(consoleIdx).toBeGreaterThan(-1); + expect(otherIdx).toBeGreaterThan(-1); + // Canonical order: Local Actor Development → Apify Console → … → OTHER. + expect(devIdx).toBeLessThan(consoleIdx); + expect(consoleIdx).toBeLessThan(otherIdx); + }); + + test('shows the apify preamble and canonical examples for the apify entrypoint', () => { + const output = stripAnsi(renderMainHelpMenu('apify')); + + expect(output).toContain('Apify command-line interface (CLI)'); + expect(output).toContain('USAGE'); + expect(output).toContain('$ apify [options]'); + expect(output).toContain('EXAMPLES'); + expect(output).toContain('$ apify login'); + expect(output).toContain("Use 'apify --help'"); + }); + + test('shows the actor preamble and canonical examples for the actor entrypoint', () => { + const output = stripAnsi(renderMainHelpMenu('actor')); + + expect(output).toContain("'actor' is the runtime CLI"); + expect(output).toContain('$ actor [options]'); + expect(output).toContain('$ actor get-input'); + expect(output).toContain("Use 'actor --help'"); + }); + }); +});