From aed96f6621402b11e4732f80352b4f1f29724acf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Thu, 16 Jul 2026 14:09:34 +0200 Subject: [PATCH 1/5] feat(create): machine-readable --json output and hidden --origin flag - Enable --json on `apify create`: prints { dir, actorJsonPath, template, source, nextSteps }. Progress/logs stay on stderr. - Add hidden --origin console|cli flag for funnel telemetry (default cli). - Extract buildNextSteps(), shared by the --json output and success message. - templates ls: render the language label ("JavaScript") not the raw category. Refs #1238 Co-Authored-By: Claude Opus 4.8 --- src/commands/create.ts | 32 ++++++++++++++++++++-- src/commands/templates/ls.ts | 3 ++- src/lib/create-utils.ts | 24 ++++++++++++----- src/lib/hooks/telemetry/trackEvent.ts | 1 + test/local/commands/create.test.ts | 38 ++++++++++++++++++++++++++- 5 files changed, 88 insertions(+), 10 deletions(-) diff --git a/src/commands/create.ts b/src/commands/create.ts index 32cef85c3..dcdbd2e52 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -18,6 +18,7 @@ import { SUPPORTED_NODEJS_VERSION, } from '../lib/consts.js'; import { + buildNextSteps, enhanceReadmeWithLocalSuffix, ensureValidActorName, formatCreateSuccessMessage, @@ -36,6 +37,7 @@ import { getJsonFileContent, isNodeVersionSupported, isPythonVersionSupported, + printJsonToStdout, setLocalConfig, setLocalEnv, } from '../lib/utils.js'; @@ -111,6 +113,13 @@ export class CreateCommand extends ApifyCommand { description: 'Skip initializing a git repository in the Actor directory.', required: false, }), + origin: Flags.string({ + description: 'Where the command was invoked from. Used for funnel telemetry.', + choices: ['console', 'cli'], + default: 'cli', + required: false, + hidden: true, + }), }; static override args = { @@ -120,9 +129,11 @@ export class CreateCommand extends ApifyCommand { }), }; + static override enableJsonFlag = true; + async run() { let { actorName } = this.args; - const { template: templateName, useCase, language, skipDependencyInstall, skipGitInit } = this.flags; + const { template: templateName, useCase, language, skipDependencyInstall, skipGitInit, origin, json } = this.flags; // --template-archive-url is an internal, undocumented flag that's used // for testing of templates that are not yet published in the manifest @@ -169,14 +180,17 @@ export class CreateCommand extends ApifyCommand { } let messages = null; + let templateId: string | null = null; this.telemetryData.create = { fromArchiveUrl: !!templateArchiveUrl, + origin: origin as 'console' | 'cli', }; if (!templateArchiveUrl) { const templateDefinition = await getTemplateDefinition(templateName, manifestPromise, { useCase, language }); ({ archiveUrl: templateArchiveUrl, messages } = templateDefinition); + templateId = templateDefinition.id; this.telemetryData.create.templateId = templateDefinition.id; this.telemetryData.create.templateName = templateDefinition.name; this.telemetryData.create.templateLanguage = templateDefinition.category; @@ -405,6 +419,20 @@ export class CreateCommand extends ApifyCommand { // Suggest install command if dependencies were not installed const installCommandSuggestion = !dependenciesInstalled ? await getInstallCommandSuggestion(actFolderDir) : null; + const gitRepositoryInitialized = !skipGitInit && !cwdHasGit && gitInitResult.success; + + // Machine-readable output for agents and other non-interactive callers. + if (json) { + printJsonToStdout({ + dir: actFolderDir, + actorJsonPath: join(actFolderDir, LOCAL_CONFIG_PATH), + template: templateId, + source: 'apify', + nextSteps: buildNextSteps({ actorName, dependenciesInstalled, installCommandSuggestion }), + }); + return; + } + // Success message with extra empty line simpleLog({ message: '' }); success({ @@ -412,7 +440,7 @@ export class CreateCommand extends ApifyCommand { actorName, dependenciesInstalled, postCreate: messages?.postCreate ?? null, - gitRepositoryInitialized: !skipGitInit && !cwdHasGit && gitInitResult.success, + gitRepositoryInitialized, installCommandSuggestion, }), }); diff --git a/src/commands/templates/ls.ts b/src/commands/templates/ls.ts index ec30eafa5..b4ad8f547 100644 --- a/src/commands/templates/ls.ts +++ b/src/commands/templates/ls.ts @@ -3,6 +3,7 @@ import { fetchManifest } from '@apify/actor-templates'; import { ApifyCommand } from '../../lib/command-framework/apify-command.js'; import { CompactMode, ResponsiveTable } from '../../lib/commands/responsive-table.js'; import { info, simpleLog } from '../../lib/outputs.js'; +import { languageLabel } from '../../lib/templates/consts.js'; import { printJsonToStdout } from '../../lib/utils.js'; const table = new ResponsiveTable({ @@ -50,7 +51,7 @@ export class TemplatesLsCommand extends ApifyCommand table.pushRow({ Template: template.name, Label: template.label, - Language: template.category, + Language: languageLabel(template.category), 'Use cases': (template.useCases ?? []).join(', '), }); } diff --git a/src/lib/create-utils.ts b/src/lib/create-utils.ts index c95d8827b..454f765d7 100644 --- a/src/lib/create-utils.ts +++ b/src/lib/create-utils.ts @@ -77,6 +77,22 @@ export async function enhanceReadmeWithLocalSuffix(readmePath: string, manifestP } } +export function buildNextSteps(params: { + actorName: string; + dependenciesInstalled: boolean; + installCommandSuggestion?: string | null; +}): string[] { + const { actorName, dependenciesInstalled, installCommandSuggestion } = params; + + const steps = [`cd "${actorName}"`]; + if (!dependenciesInstalled) { + steps.push(installCommandSuggestion || 'install dependencies with your package manager'); + } + steps.push('apify run'); + + return steps; +} + export function formatCreateSuccessMessage(params: { actorName: string; dependenciesInstalled: boolean; @@ -88,12 +104,8 @@ export function formatCreateSuccessMessage(params: { let message = `āœ… Actor '${actorName}' created successfully!`; - if (dependenciesInstalled) { - message += `\n\nNext steps:\n\ncd "${actorName}"\napify run`; - } else { - const installLine = installCommandSuggestion || 'install dependencies with your package manager'; - message += `\n\nNext steps:\n\ncd "${actorName}"\n${installLine}\napify run`; - } + const nextSteps = buildNextSteps({ actorName, dependenciesInstalled, installCommandSuggestion }); + message += `\n\nNext steps:\n\n${nextSteps.join('\n')}`; message += `\n\nšŸ’” Tip: Use 'apify push' to deploy your Actor to the Apify platform\nšŸ“– Docs: https://docs.apify.com/platform/actors/development`; diff --git a/src/lib/hooks/telemetry/trackEvent.ts b/src/lib/hooks/telemetry/trackEvent.ts index e801d35a0..864d7ded1 100644 --- a/src/lib/hooks/telemetry/trackEvent.ts +++ b/src/lib/hooks/telemetry/trackEvent.ts @@ -33,6 +33,7 @@ interface CliCommandEvent { templateId?: string; templateName?: string; templateLanguage?: string; + origin?: 'console' | 'cli'; }; push?: { diff --git a/test/local/commands/create.test.ts b/test/local/commands/create.test.ts index 2dfa3a189..7e10a52e2 100644 --- a/test/local/commands/create.test.ts +++ b/test/local/commands/create.test.ts @@ -18,7 +18,7 @@ const { beforeAllCalls, afterAllCalls, joinPath, joinCwdPath, toggleCwdBetweenFu cwdParent: true, }); -const { lastErrorMessage } = useConsoleSpy(); +const { lastErrorMessage, logMessages } = useConsoleSpy(); const { CreateCommand } = await import('../../../src/commands/create.js'); @@ -126,6 +126,42 @@ describe('apify create', () => { ).to.be.eql(expectedInput); }); + it('prints a machine-readable contract with --json', async () => { + await testRunCommand(CreateCommand, { + args_actorName: actName, + flags_template: 'project_empty', + flags_skipDependencyInstall: true, + flags_skipGitInit: true, + flags_json: true, + }); + + const jsonLine = logMessages.log.find((message) => message.trim().startsWith('{')); + expect(jsonLine).toBeDefined(); + + const output = JSON.parse(jsonLine!); + expect(output.source).toBe('apify'); + expect(typeof output.template).toBe('string'); + expect(output.dir.endsWith(actName)).toBe(true); + expect(output.actorJsonPath.endsWith(LOCAL_CONFIG_PATH)).toBe(true); + expect(output.nextSteps[0]).toBe(`cd "${actName}"`); + expect(output.nextSteps).toContain('apify run'); + }); + + it('accepts the hidden --origin flag and still emits the --json contract', async () => { + await testRunCommand(CreateCommand, { + args_actorName: actName, + flags_template: 'project_empty', + flags_skipDependencyInstall: true, + flags_skipGitInit: true, + flags_origin: 'console', + flags_json: true, + }); + + const jsonLine = logMessages.log.find((message) => message.trim().startsWith('{')); + expect(jsonLine).toBeDefined(); + expect(JSON.parse(jsonLine!).source).toBe('apify'); + }); + it('should skip installing optional dependencies', async () => { const ACT_TEMPLATE = 'project_cheerio_crawler_js'; From c3d7eae29864ce9826124a5cac9ceab361f80b63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Sol=C3=A1r?= Date: Thu, 30 Jul 2026 18:04:36 +0200 Subject: [PATCH 2/5] fix(create): keep --json stdout machine-parseable Child process output was inherited onto the CLI's own stdout, so `git init` and the dependency installer transcript landed ahead of the JSON payload. `apify create --json | jq` failed on every invocation that did not pass both --skip-git-init and --skip-dependency-install, including the bare --json call that agents and the Console "Clone locally" handoff actually make. Add keepStdoutClean() to route child stdout to stderr, latched by the command framework whenever --json is set, so it covers every command with enableJsonFlag rather than just this one call site. Also include postCreate in the payload: 9 templates set it and two (python-playwright, python-selenium) document hard prerequisites, so an agent following nextSteps alone ended up with a broken Actor. Add gitRepositoryInitialized so a failed git init is visible to machine callers. The tests searched for the JSON among other stdout lines, which tolerated the pollution; they now require stdout to be exactly the payload. --origin is asserted through telemetry instead of a duplicated payload check. Co-Authored-By: Claude Opus 5 --- docs/reference.md | 4 ++- skills/apify/SKILL.md | 1 + src/commands/create.ts | 38 +++++++++++----------- src/lib/command-framework/apify-command.ts | 5 +++ src/lib/exec.ts | 11 ++++++- src/lib/templates/consts.ts | 2 +- test/local/commands/create.test.ts | 38 +++++++++++++++++----- test/local/lib/exec.test.ts | 37 +++++++++++++++++++++ 8 files changed, 106 insertions(+), 30 deletions(-) create mode 100644 test/local/lib/exec.test.ts diff --git a/docs/reference.md b/docs/reference.md index 09308b0d3..44803367c 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -277,7 +277,7 @@ DESCRIPTION directory. USAGE - $ apify create [actorName] + $ apify create [actorName] [--json] [-l javascript|js|typescript|ts|python|py] [--omit-optional-deps] [--skip-dependency-install] [--skip-git-init] [-t ] @@ -287,6 +287,8 @@ ARGUMENTS actorName Name of the Actor and its directory. FLAGS + --json Format the command + output as JSON. -l, --language=