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
118 changes: 91 additions & 27 deletions src/commands/create.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { mkdir, readdir, stat } from 'node:fs/promises';
import { join } from 'node:path';
import { basename, join } from 'node:path';
import process from 'node:process';

import { gte, minVersion } from 'semver';
Expand Down Expand Up @@ -34,6 +34,7 @@ import {
getJsonFileContent,
isNodeVersionSupported,
isPythonVersionSupported,
sanitizeActorName,
setLocalConfig,
setLocalEnv,
} from '../lib/utils.js';
Expand Down Expand Up @@ -64,6 +65,15 @@ export class CreateCommand extends ApifyCommand<typeof CreateCommand> {
description: 'Create without installing dependencies (faster; run install yourself later).',
command: 'apify create my-actor --template python-start --skip-dependency-install',
},
{
description:
'Scaffold into the current directory (no wrapper subdirectory). The Actor name is derived from the cwd basename.',
command: 'apify create . --template js-crawlee-cheerio',
},
{
description: 'Same as above, using the --here flag explicitly.',
command: 'apify create --here --template js-crawlee-cheerio',
},
];

static override docsUrl = 'https://docs.apify.com/cli/docs/reference#apify-create';
Expand Down Expand Up @@ -92,6 +102,20 @@ export class CreateCommand extends ApifyCommand<typeof CreateCommand> {
description: 'Skip initializing a git repository in the Actor directory.',
required: false,
}),
here: Flags.boolean({
description:
'Scaffold the Actor into the current directory instead of creating a subdirectory. ' +
'The Actor name is derived from the current directory basename unless explicitly provided. ' +
'Refuses to overwrite a non-empty directory unless --force is set. ' +
'You can also pass "." as the positional argument for the same effect.',
required: false,
}),
force: Flags.boolean({
description:
'Allow scaffolding into a non-empty current directory when combined with --here (or "."). ' +
'Existing files will not be deleted, but template files may overwrite them.',
required: false,
}),
};

static override args = {
Expand All @@ -103,7 +127,7 @@ export class CreateCommand extends ApifyCommand<typeof CreateCommand> {

async run() {
let { actorName } = this.args;
const { template: templateName, skipDependencyInstall, skipGitInit } = this.flags;
const { template: templateName, skipDependencyInstall, skipGitInit, here, force } = 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 All @@ -116,37 +140,73 @@ export class CreateCommand extends ApifyCommand<typeof CreateCommand> {
return new Error(`Could not fetch template list from server. Cause: ${err?.message}`);
});

actorName = await ensureValidActorName(actorName);

const cwd = process.cwd();
let actFolderDir = join(cwd, actorName);

while (true) {
const folderExists = await stat(actFolderDir).catch(() => null);
const folderHasFiles =
folderExists &&
(await readdir(actFolderDir)
.then((files) => files.length > 0)
.catch(() => false));
// Detect "scaffold into current directory" mode: either `apify create .` or `--here`.
const scaffoldHere = here || actorName === '.';

let actFolderDir: string;

if (folderExists?.isDirectory() && folderHasFiles) {
if (scaffoldHere) {
// When scaffolding into cwd, derive the Actor name from the cwd basename
// unless an explicit non-"." name was provided.
if (!actorName || actorName === '.') {
const derived = sanitizeActorName(basename(cwd));
actorName = await ensureValidActorName(derived);
} else {
actorName = await ensureValidActorName(actorName);
}

actFolderDir = cwd;

const cwdFiles = await readdir(cwd).catch(() => [] as string[]);
if (cwdFiles.length > 0 && !force) {
error({
message:
`Cannot create new Actor, directory '${actorName}' already exists. Please provide a different name.` +
' You can use "apify init" to create a local Actor environment inside an existing directory.',
`Cannot scaffold into '${cwd}': directory is not empty. ` +
'Re-run with --force to scaffold anyway (template files may overwrite existing files), ' +
'or use "apify init" to add Actor scaffolding to an existing project.',
});

actorName = await ensureValidActorName();
actFolderDir = join(cwd, actorName);

continue;
return;
}
if (cwdFiles.length > 0 && force) {
warning({
message:
`Scaffolding into non-empty directory '${cwd}' because --force was set. ` +
'Template files may overwrite existing files.',
});
}
} else {
actorName = await ensureValidActorName(actorName);
actFolderDir = join(cwd, actorName);

while (true) {
const folderExists = await stat(actFolderDir).catch(() => null);
const folderHasFiles =
folderExists &&
(await readdir(actFolderDir)
.then((files) => files.length > 0)
.catch(() => false));

if (folderExists?.isDirectory() && folderHasFiles) {
error({
message:
`Cannot create new Actor, directory '${actorName}' already exists. Please provide a different name.` +
' You can use "apify init" to create a local Actor environment inside an existing directory.',
});

actorName = await ensureValidActorName();
actFolderDir = join(cwd, actorName);

continue;
}

// Create Actor directory structure
if (!folderExists) {
await mkdir(actFolderDir, { recursive: true });
// Create Actor directory structure
if (!folderExists) {
await mkdir(actFolderDir, { recursive: true });
}
break;
}
break;
}

let messages = null;
Expand Down Expand Up @@ -339,9 +399,12 @@ export class CreateCommand extends ApifyCommand<typeof CreateCommand> {

// Initialize git repository before reporting success, but store result for later
let gitInitResult: { success: boolean; error?: Error } = { success: true };
// Check whether git is already initialized in a parent (for subdir mode) or in cwd itself.
// When scaffolding into cwd, checking `cwd/.git` correctly detects an existing repo.
const cwdHasGit = await stat(join(cwd, '.git')).catch(() => null);
const actFolderHasGit = scaffoldHere ? cwdHasGit : await stat(join(actFolderDir, '.git')).catch(() => null);

if (!skipGitInit && !cwdHasGit) {
if (!skipGitInit && !cwdHasGit && !actFolderHasGit) {
try {
await execWithLog({
cmd: 'git',
Expand All @@ -363,13 +426,14 @@ export class CreateCommand extends ApifyCommand<typeof CreateCommand> {
actorName,
dependenciesInstalled,
postCreate: messages?.postCreate ?? null,
gitRepositoryInitialized: !skipGitInit && !cwdHasGit && gitInitResult.success,
gitRepositoryInitialized: !skipGitInit && !cwdHasGit && !actFolderHasGit && gitInitResult.success,
installCommandSuggestion,
scaffoldedIntoCwd: scaffoldHere,
}),
});

// Report git initialization result only if it failed (success already included in success message)
if (!skipGitInit && !cwdHasGit && !gitInitResult.success) {
if (!skipGitInit && !cwdHasGit && !actFolderHasGit && !gitInitResult.success) {
// Git init is not critical, so we just warn if it fails
warning({ message: `Failed to initialize git repository: ${gitInitResult.error!.message}` });
warning({ message: 'You can manually run "git init" in the Actor directory if needed.' });
Expand Down
21 changes: 17 additions & 4 deletions src/lib/create-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,22 +71,35 @@ export function formatCreateSuccessMessage(params: {
postCreate?: string | null;
gitRepositoryInitialized?: boolean;
installCommandSuggestion?: string | null;
scaffoldedIntoCwd?: boolean;
}) {
const { actorName, dependenciesInstalled, postCreate, gitRepositoryInitialized, installCommandSuggestion } = params;
const {
actorName,
dependenciesInstalled,
postCreate,
gitRepositoryInitialized,
installCommandSuggestion,
scaffoldedIntoCwd,
} = params;

let message = `✅ Actor '${actorName}' created successfully!`;

// When scaffolded into the current directory, skip the `cd` hint.
const cdLine = scaffoldedIntoCwd ? '' : `cd "${actorName}"\n`;

if (dependenciesInstalled) {
message += `\n\nNext steps:\n\ncd "${actorName}"\napify run`;
message += `\n\nNext steps:\n\n${cdLine}apify run`;
} else {
const installLine = installCommandSuggestion || 'install dependencies with your package manager';
message += `\n\nNext steps:\n\ncd "${actorName}"\n${installLine}\napify run`;
message += `\n\nNext steps:\n\n${cdLine}${installLine}\napify run`;
}

message += `\n\n💡 Tip: Use 'apify push' to deploy your Actor to the Apify platform\n📖 Docs: https://docs.apify.com/platform/actors/development`;

if (gitRepositoryInitialized) {
message += `\n🌱 Git repository initialized in '${actorName}'. You can now commit and push your Actor to Git.`;
message += scaffoldedIntoCwd
? `\n🌱 Git repository initialized in the current directory. You can now commit and push your Actor to Git.`
: `\n🌱 Git repository initialized in '${actorName}'. You can now commit and push your Actor to Git.`;
}

if (postCreate) {
Expand Down
Loading