Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
192 lines
6.1 KiB
TypeScript
192 lines
6.1 KiB
TypeScript
import { confirm, intro, isCancel, log, note, outro, spinner } from '@clack/prompts';
|
|
import { Args, Command, Flags } from '@oclif/core';
|
|
import { camelCase } from 'change-case';
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
|
|
import {
|
|
chatModelTypePrompt,
|
|
declarativeTemplatePrompt,
|
|
nodeNamePrompt,
|
|
nodeTypePrompt,
|
|
programmaticNodeTypePrompt,
|
|
} from './prompts';
|
|
import { createIntro } from './utils';
|
|
import type { TemplateData, TemplateWithRun } from '../../template/core';
|
|
import { getTemplate, isTemplateName, isTemplateType, templates } from '../../template/templates';
|
|
import { ChildProcessError, runCommand } from '../../utils/child-process';
|
|
import { delayAtLeast, folderExists } from '../../utils/filesystem';
|
|
import { initGit, tryReadGitUser } from '../../utils/git';
|
|
import { detectPackageManager } from '../../utils/package-manager';
|
|
import { onCancel } from '../../utils/prompts';
|
|
import { validateNodeName } from '../../utils/validation';
|
|
|
|
export default class New extends Command {
|
|
static override description = 'Create a starter community node in a new directory';
|
|
static override examples = [
|
|
'<%= config.bin %> <%= command.id %>',
|
|
'<%= config.bin %> <%= command.id %> n8n-nodes-my-app --skip-install',
|
|
'<%= config.bin %> <%= command.id %> n8n-nodes-my-app --force',
|
|
'<%= config.bin %> <%= command.id %> n8n-nodes-my-app --template declarative/custom',
|
|
];
|
|
static override args = {
|
|
name: Args.string({ name: 'Name' }),
|
|
};
|
|
static override flags = {
|
|
force: Flags.boolean({
|
|
char: 'f',
|
|
description: 'Overwrite destination folder if it already exists',
|
|
}),
|
|
'skip-install': Flags.boolean({ description: 'Skip installing dependencies' }),
|
|
template: Flags.string({
|
|
options: [
|
|
'declarative/github-issues',
|
|
'declarative/custom',
|
|
'programmatic/example',
|
|
'programmatic/openai-chat-model',
|
|
'programmatic/custom-chat-model',
|
|
'programmatic/custom-chat-model-example',
|
|
'programmatic/custom-chat-memory',
|
|
] as const,
|
|
}),
|
|
};
|
|
|
|
async run(): Promise<void> {
|
|
const { flags, args } = await this.parse(New);
|
|
const [typeFlag, templateFlag] = flags.template?.split('/') ?? [];
|
|
|
|
intro(await createIntro());
|
|
|
|
const nodeName = args.name ?? (await nodeNamePrompt());
|
|
const invalidNodeNameError = validateNodeName(nodeName);
|
|
|
|
if (invalidNodeNameError) return onCancel(invalidNodeNameError);
|
|
|
|
const destination = path.resolve(process.cwd(), nodeName);
|
|
|
|
let overwrite = false;
|
|
if (await folderExists(destination)) {
|
|
if (!flags.force) {
|
|
const shouldOverwrite = await confirm({
|
|
message: `./${nodeName} already exists, do you want to overwrite?`,
|
|
});
|
|
if (isCancel(shouldOverwrite) || !shouldOverwrite) return onCancel();
|
|
}
|
|
|
|
overwrite = true;
|
|
}
|
|
|
|
const type = typeFlag ?? (await nodeTypePrompt());
|
|
if (!isTemplateType(type)) {
|
|
return onCancel(`Invalid template type: ${type}`);
|
|
}
|
|
|
|
let template: TemplateWithRun = templates.programmatic.example;
|
|
if (templateFlag) {
|
|
const name = camelCase(templateFlag);
|
|
if (!isTemplateName(type, name)) {
|
|
return onCancel(`Invalid template name: ${name} for type: ${type}`);
|
|
}
|
|
template = getTemplate(type, name);
|
|
} else if (type === 'declarative') {
|
|
const chosenTemplate = await declarativeTemplatePrompt();
|
|
template = getTemplate('declarative', chosenTemplate) as TemplateWithRun;
|
|
} else if (type === 'programmatic') {
|
|
const programmaticNodeType = await programmaticNodeTypePrompt();
|
|
|
|
if (programmaticNodeType === 'basic') {
|
|
template = templates.programmatic.example;
|
|
} else if (programmaticNodeType === 'chatModel') {
|
|
const chatModelType = await chatModelTypePrompt();
|
|
if (chatModelType === 'openaiCompatible') {
|
|
template = templates.programmatic.openaiChatModel as TemplateWithRun;
|
|
} else if (chatModelType === 'custom') {
|
|
template = templates.programmatic.customChatModel as TemplateWithRun;
|
|
} else if (chatModelType === 'customExample') {
|
|
template = templates.programmatic.customChatModelExample as TemplateWithRun;
|
|
}
|
|
} else if (programmaticNodeType === 'chatMemory') {
|
|
template = templates.programmatic.customChatMemory as TemplateWithRun;
|
|
}
|
|
}
|
|
|
|
const config = (await template.prompts?.()) ?? {};
|
|
const packageManager = (await detectPackageManager()) ?? 'npm';
|
|
const templateData: TemplateData = {
|
|
destinationPath: destination,
|
|
nodePackageName: nodeName,
|
|
config,
|
|
user: tryReadGitUser(),
|
|
packageManager: {
|
|
name: packageManager,
|
|
installCommand: packageManager === 'npm' ? 'ci' : 'install',
|
|
},
|
|
};
|
|
const copyingSpinner = spinner();
|
|
copyingSpinner.start('Copying files');
|
|
if (overwrite) {
|
|
await fs.rm(destination, { recursive: true, force: true });
|
|
}
|
|
await delayAtLeast(template.run(templateData), 1000);
|
|
copyingSpinner.stop('Files copied');
|
|
|
|
const gitSpinner = spinner();
|
|
gitSpinner.start('Initializing git repository');
|
|
|
|
try {
|
|
await initGit(destination);
|
|
|
|
gitSpinner.stop('Git repository initialized');
|
|
} catch (error: unknown) {
|
|
if (error instanceof ChildProcessError) {
|
|
gitSpinner.stop(
|
|
`Could not initialize git repository: ${error.message}`,
|
|
error.code ?? undefined,
|
|
);
|
|
process.exit(error.code ?? 1);
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
if (!flags['skip-install']) {
|
|
const installingSpinner = spinner();
|
|
installingSpinner.start('Installing dependencies');
|
|
|
|
try {
|
|
await delayAtLeast(
|
|
runCommand(packageManager, ['install'], {
|
|
cwd: destination,
|
|
printOutput: ({ stdout, stderr }) => {
|
|
log.error(stdout.concat(stderr).toString());
|
|
},
|
|
}),
|
|
1000,
|
|
);
|
|
} catch (error: unknown) {
|
|
if (error instanceof ChildProcessError) {
|
|
installingSpinner.stop(
|
|
`Could not install dependencies: ${error.message}`,
|
|
error.code ?? undefined,
|
|
);
|
|
process.exit(error.code ?? 1);
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
installingSpinner.stop('Dependencies installed');
|
|
}
|
|
|
|
note(
|
|
`cd ./${nodeName} && ${packageManager} run dev
|
|
|
|
📚 Documentation: https://docs.n8n.io/integrations/creating-nodes/build/${type}-style-node/
|
|
💬 Community: https://community.n8n.io`,
|
|
'Next Steps',
|
|
);
|
|
|
|
outro(`Created ./${nodeName} ✨`);
|
|
}
|
|
}
|