mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 09:36:44 +00:00
fix: Improve Node CLI error messages and dev command exit behavior (#19637)
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@n8n/create-node",
|
"name": "@n8n/create-node",
|
||||||
"version": "0.7.0",
|
"version": "0.8.0",
|
||||||
"description": "Official CLI to create new community nodes for n8n",
|
"description": "Official CLI to create new community nodes for n8n",
|
||||||
"bin": {
|
"bin": {
|
||||||
"create-node": "bin/create-node.cjs"
|
"create-node": "bin/create-node.cjs"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@n8n/node-cli",
|
"name": "@n8n/node-cli",
|
||||||
"version": "0.7.0",
|
"version": "0.8.0",
|
||||||
"description": "Official CLI for developing community nodes for n8n",
|
"description": "Official CLI for developing community nodes for n8n",
|
||||||
"bin": {
|
"bin": {
|
||||||
"n8n-node": "bin/n8n-node.mjs"
|
"n8n-node": "bin/n8n-node.mjs"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { intro, outro, spinner, log } from '@clack/prompts';
|
import { intro, log, outro, spinner } from '@clack/prompts';
|
||||||
import { Command, Flags } from '@oclif/core';
|
import { Command, Flags } from '@oclif/core';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
@@ -29,7 +29,7 @@ export default class Dev extends Command {
|
|||||||
'custom-user-folder': Flags.directory({
|
'custom-user-folder': Flags.directory({
|
||||||
default: path.join(os.homedir(), '.n8n-node-cli'),
|
default: path.join(os.homedir(), '.n8n-node-cli'),
|
||||||
description:
|
description:
|
||||||
'Folder to use to store user-specific n8n data. By default it will use ~/.n8n-node-cli.',
|
'Folder to use to store user-specific n8n data. By default it will use ~/.n8n-node-cli. The node CLI will install your node here.',
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -74,41 +74,44 @@ export default class Dev extends Command {
|
|||||||
log.warn(picocolors.dim('First run may take a few minutes while dependencies are installed'));
|
log.warn(picocolors.dim('First run may take a few minutes while dependencies are installed'));
|
||||||
|
|
||||||
// Run n8n with hot reload enabled, always attempt to use latest n8n
|
// Run n8n with hot reload enabled, always attempt to use latest n8n
|
||||||
// TODO: Use n8n@latest. Currently using n8n@next because of broken hot reloading before n8n@1.111.0
|
try {
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
new Promise<void>((resolve) => {
|
new Promise<void>((resolve) => {
|
||||||
runPersistentCommand('npx', ['-y', '--quiet', '--prefer-online', 'n8n@next'], {
|
runPersistentCommand('npx', ['-y', '--quiet', '--prefer-online', 'n8n@latest'], {
|
||||||
cwd: n8nUserFolder,
|
cwd: n8nUserFolder,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
N8N_DEV_RELOAD: 'true',
|
N8N_DEV_RELOAD: 'true',
|
||||||
N8N_RUNNERS_ENABLED: 'true',
|
N8N_RUNNERS_ENABLED: 'true',
|
||||||
DB_SQLITE_POOL_SIZE: '10',
|
DB_SQLITE_POOL_SIZE: '10',
|
||||||
N8N_USER_FOLDER: n8nUserFolder,
|
N8N_USER_FOLDER: n8nUserFolder,
|
||||||
N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS: 'false',
|
},
|
||||||
},
|
name: 'n8n',
|
||||||
name: 'n8n',
|
color: picocolors.green,
|
||||||
color: picocolors.green,
|
allowOutput: (line) => {
|
||||||
allowOutput: (line) => {
|
if (line.includes('Initializing n8n process')) {
|
||||||
if (line.includes('Initializing n8n process')) {
|
resolve();
|
||||||
resolve();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return setupComplete;
|
return setupComplete;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
new Promise<void>((_, reject) => {
|
new Promise<void>((_, reject) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const error = new Error('n8n startup timeout after 120 seconds');
|
const error = new Error('n8n startup timeout after 120 seconds');
|
||||||
onCancel(error.message);
|
reject(error);
|
||||||
reject(error);
|
}, 120_000);
|
||||||
}, 120_000);
|
}),
|
||||||
}),
|
]);
|
||||||
]);
|
|
||||||
|
|
||||||
setupComplete = true;
|
setupComplete = true;
|
||||||
npxN8nSpinner.stop('Started n8n dev server');
|
npxN8nSpinner.stop('Started n8n dev server');
|
||||||
|
} catch (error) {
|
||||||
|
npxN8nSpinner.stop('Failed to start n8n dev server');
|
||||||
|
onCancel(error instanceof Error ? error.message : 'Unknown error occurred', 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
outro('✓ Setup complete');
|
outro('✓ Setup complete');
|
||||||
|
|||||||
@@ -7,22 +7,100 @@ import { jsonParse } from '../../utils/json';
|
|||||||
|
|
||||||
export function commands() {
|
export function commands() {
|
||||||
const childProcesses: ChildProcess[] = [];
|
const childProcesses: ChildProcess[] = [];
|
||||||
|
let isShuttingDown = false;
|
||||||
|
|
||||||
function registerChild(child: ChildProcess) {
|
const registerChild = (child: ChildProcess): void => {
|
||||||
childProcesses.push(child);
|
childProcesses.push(child);
|
||||||
}
|
};
|
||||||
|
|
||||||
function cleanup(signal: 'SIGINT' | 'SIGTERM') {
|
const killChild = (child: ChildProcess, signal: NodeJS.Signals): void => {
|
||||||
for (const child of childProcesses) {
|
if (!child.killed) {
|
||||||
child.kill(signal);
|
child.kill(signal);
|
||||||
}
|
}
|
||||||
process.exit();
|
};
|
||||||
}
|
|
||||||
|
|
||||||
process.on('SIGINT', () => cleanup('SIGINT'));
|
const forceKillAllChildren = (): void => {
|
||||||
process.on('SIGTERM', () => cleanup('SIGTERM'));
|
childProcesses.forEach((child) => killChild(child, 'SIGKILL'));
|
||||||
|
process.exit(1);
|
||||||
|
};
|
||||||
|
|
||||||
function runPersistentCommand(
|
const gracefulShutdown = (signal: 'SIGINT' | 'SIGTERM'): void => {
|
||||||
|
if (childProcesses.length === 0) {
|
||||||
|
process.exit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let exitedCount = 0;
|
||||||
|
const totalChildren = childProcesses.length;
|
||||||
|
|
||||||
|
const forceExitTimer = setTimeout(forceKillAllChildren, 5000);
|
||||||
|
|
||||||
|
const onChildExit = () => {
|
||||||
|
exitedCount++;
|
||||||
|
if (exitedCount === totalChildren) {
|
||||||
|
clearTimeout(forceExitTimer);
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
childProcesses.forEach((child) => {
|
||||||
|
if (!child.killed) {
|
||||||
|
child.once('exit', onChildExit);
|
||||||
|
killChild(child, signal);
|
||||||
|
|
||||||
|
// Escalate to SIGKILL after 5 seconds
|
||||||
|
setTimeout(() => killChild(child, 'SIGKILL'), 5000);
|
||||||
|
} else {
|
||||||
|
onChildExit(); // Process already dead
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSignal = (signal: 'SIGINT' | 'SIGTERM'): void => {
|
||||||
|
if (isShuttingDown) {
|
||||||
|
// Second signal - force kill immediately
|
||||||
|
console.log('\nForce killing processes...');
|
||||||
|
forceKillAllChildren();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isShuttingDown = true;
|
||||||
|
if (signal === 'SIGINT') {
|
||||||
|
console.log('\nShutting down gracefully... (press Ctrl+C again to force quit)');
|
||||||
|
}
|
||||||
|
gracefulShutdown(signal);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('SIGINT', () => handleSignal('SIGINT'));
|
||||||
|
process.on('SIGTERM', () => handleSignal('SIGTERM'));
|
||||||
|
|
||||||
|
const stripAnsiCodes = (input: string): string =>
|
||||||
|
input
|
||||||
|
.replace(/\x1Bc/g, '') // Full reset
|
||||||
|
.replace(/\x1B\[2J/g, '') // Clear screen
|
||||||
|
.replace(/\x1B\[3J/g, '') // Clear scrollback
|
||||||
|
.replace(/\x1B\[H/g, '') // Move cursor to top-left
|
||||||
|
.replace(/\x1B\[0?m/g, ''); // Reset colors
|
||||||
|
|
||||||
|
const createLogger =
|
||||||
|
(name?: string, color?: Formatter, allowOutput?: (line: string) => boolean) =>
|
||||||
|
(text: string): void => {
|
||||||
|
if (allowOutput && !allowOutput(text)) return;
|
||||||
|
|
||||||
|
const prefix = name ? (color ? color(`[${name}]`) : `[${name}]`) : '';
|
||||||
|
console.log(prefix ? `${prefix} ${text}` : text);
|
||||||
|
};
|
||||||
|
|
||||||
|
const processOutput = (data: Buffer, logger: (text: string) => void): void => {
|
||||||
|
data
|
||||||
|
.toString()
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => stripAnsiCodes(line).trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.forEach(logger);
|
||||||
|
};
|
||||||
|
|
||||||
|
const runPersistentCommand = (
|
||||||
cmd: string,
|
cmd: string,
|
||||||
args: string[],
|
args: string[],
|
||||||
opts: {
|
opts: {
|
||||||
@@ -32,7 +110,7 @@ export function commands() {
|
|||||||
color?: Formatter;
|
color?: Formatter;
|
||||||
allowOutput?: (line: string) => boolean;
|
allowOutput?: (line: string) => boolean;
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
): ChildProcess => {
|
||||||
const child = spawn(cmd, args, {
|
const child = spawn(cmd, args, {
|
||||||
cwd: opts.cwd,
|
cwd: opts.cwd,
|
||||||
env: { ...process.env, ...opts.env },
|
env: { ...process.env, ...opts.env },
|
||||||
@@ -41,46 +119,21 @@ export function commands() {
|
|||||||
|
|
||||||
registerChild(child);
|
registerChild(child);
|
||||||
|
|
||||||
function stripClearCodes(input: string): string {
|
const logger = createLogger(opts.name, opts.color, opts.allowOutput);
|
||||||
// Remove clear screen/reset ANSI codes
|
const handleOutput = (data: Buffer) => processOutput(data, logger);
|
||||||
return input
|
|
||||||
.replace(/\x1Bc/g, '') // Full reset
|
|
||||||
.replace(/\x1B\[2J/g, '') // Clear screen
|
|
||||||
.replace(/\x1B\[3J/g, '') // Clear scrollback
|
|
||||||
.replace(/\x1B\[H/g, '') // Move cursor to top-left
|
|
||||||
.replace(/\x1B\[0?m/g, ''); // Reset colors
|
|
||||||
}
|
|
||||||
|
|
||||||
const log = (text: string) => {
|
child.stdout?.on('data', handleOutput);
|
||||||
if (opts.allowOutput && !opts.allowOutput(text)) return;
|
child.stderr?.on('data', handleOutput);
|
||||||
if (opts.name) {
|
|
||||||
const rawPrefix = `[${opts.name}]`;
|
|
||||||
const prefix = opts.color ? opts.color(rawPrefix) : rawPrefix;
|
|
||||||
console.log(`${prefix} ${text}`);
|
|
||||||
} else {
|
|
||||||
console.log(text);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOutput = (data: Buffer): void => {
|
|
||||||
data
|
|
||||||
.toString()
|
|
||||||
.split('\n')
|
|
||||||
.map((line) => stripClearCodes(line).trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
.forEach((line) => log(line));
|
|
||||||
};
|
|
||||||
|
|
||||||
child.stdout.on('data', handleOutput);
|
|
||||||
child.stderr.on('data', handleOutput);
|
|
||||||
|
|
||||||
child.on('close', (code) => {
|
child.on('close', (code) => {
|
||||||
console.log(`${opts.name ?? cmd} exited with code ${code}`);
|
if (!isShuttingDown) {
|
||||||
process.exit(code);
|
console.log(`${opts.name ?? cmd} exited with code ${code}`);
|
||||||
|
process.exit(code ?? 0);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return child;
|
return child;
|
||||||
}
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
runPersistentCommand,
|
runPersistentCommand,
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import { validateNodeName } from '../../utils/validation';
|
|||||||
export const nodeNamePrompt = async () =>
|
export const nodeNamePrompt = async () =>
|
||||||
await withCancelHandler(
|
await withCancelHandler(
|
||||||
text({
|
text({
|
||||||
message: 'What is your node called?',
|
message: "Package name (must start with 'n8n-nodes-' or '@org/n8n-nodes-')",
|
||||||
placeholder: 'n8n-nodes-example',
|
placeholder: 'n8n-nodes-my-app',
|
||||||
validate: validateNodeName,
|
validate: validateNodeName,
|
||||||
defaultValue: 'n8n-nodes-example',
|
defaultValue: 'n8n-nodes-my-app',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const validateNodeName = (name: string): string | undefined => {
|
|||||||
const regexUnscoped = /^n8n-nodes-([a-z0-9]+(?:-[a-z0-9]+)*)$/;
|
const regexUnscoped = /^n8n-nodes-([a-z0-9]+(?:-[a-z0-9]+)*)$/;
|
||||||
|
|
||||||
if (!regexScoped.test(name) && !regexUnscoped.test(name)) {
|
if (!regexScoped.test(name) && !regexUnscoped.test(name)) {
|
||||||
return 'Node name should be in the format @org/n8n-nodes-example or n8n-nodes-example';
|
return "Must start with 'n8n-nodes-' or '@org/n8n-nodes-'. Examples: n8n-nodes-my-app, @mycompany/n8n-nodes-my-app";
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user