Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ DESCRIPTION
directory.

USAGE
$ apify create [actorName]
$ apify create [actorName] [--json]
[-l javascript|typescript|python|other] [--omit-optional-deps]
[--skip-dependency-install] [--skip-git-init] [-t <value>]
[-u web-scraping|ai-agent|api-pipeline|browser-automation]
Expand All @@ -286,6 +286,8 @@ ARGUMENTS
actorName Name of the Actor and its directory.

FLAGS
--json Format the command
output as JSON.
-l, --language=<option> Filter templates by
programming language. Ignored when --template is
provided.
Expand Down
32 changes: 30 additions & 2 deletions src/commands/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
SUPPORTED_NODEJS_VERSION,
} from '../lib/consts.js';
import {
buildNextSteps,
enhanceReadmeWithLocalSuffix,
ensureValidActorName,
formatCreateSuccessMessage,
Expand All @@ -35,6 +36,7 @@ import {
getJsonFileContent,
isNodeVersionSupported,
isPythonVersionSupported,
printJsonToStdout,
setLocalConfig,
setLocalEnv,
} from '../lib/utils.js';
Expand Down Expand Up @@ -106,6 +108,13 @@ export class CreateCommand extends ApifyCommand<typeof CreateCommand> {
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 = {
Expand All @@ -115,9 +124,11 @@ export class CreateCommand extends ApifyCommand<typeof CreateCommand> {
}),
};

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
Expand Down Expand Up @@ -164,14 +175,17 @@ export class CreateCommand extends ApifyCommand<typeof CreateCommand> {
}

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;
Expand Down Expand Up @@ -370,14 +384,28 @@ export class CreateCommand extends ApifyCommand<typeof CreateCommand> {
// 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({
message: formatCreateSuccessMessage({
actorName,
dependenciesInstalled,
postCreate: messages?.postCreate ?? null,
gitRepositoryInitialized: !skipGitInit && !cwdHasGit && gitInitResult.success,
gitRepositoryInitialized,
installCommandSuggestion,
}),
});
Expand Down
3 changes: 2 additions & 1 deletion src/commands/templates/ls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -50,7 +51,7 @@ export class TemplatesLsCommand extends ApifyCommand<typeof TemplatesLsCommand>
table.pushRow({
Template: template.name,
Label: template.label,
Language: template.category,
Language: languageLabel(template.category),
'Use cases': (template.useCases ?? []).join(', '),
});
}
Expand Down
24 changes: 18 additions & 6 deletions src/lib/create-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,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;
Expand All @@ -81,12 +97,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`;

Expand Down
1 change: 1 addition & 0 deletions src/lib/hooks/telemetry/trackEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ interface CliCommandEvent {
templateId?: string;
templateName?: string;
templateLanguage?: string;
origin?: 'console' | 'cli';
};

push?: {
Expand Down
38 changes: 37 additions & 1 deletion test/local/commands/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down Expand Up @@ -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';

Expand Down