mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 01:26: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",
|
||||
"version": "0.7.0",
|
||||
"version": "0.8.0",
|
||||
"description": "Official CLI to create new community nodes for n8n",
|
||||
"bin": {
|
||||
"create-node": "bin/create-node.cjs"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@n8n/node-cli",
|
||||
"version": "0.7.0",
|
||||
"version": "0.8.0",
|
||||
"description": "Official CLI for developing community nodes for n8n",
|
||||
"bin": {
|
||||
"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 os from 'node:os';
|
||||
import path from 'node:path';
|
||||
@@ -29,7 +29,7 @@ export default class Dev extends Command {
|
||||
'custom-user-folder': Flags.directory({
|
||||
default: path.join(os.homedir(), '.n8n-node-cli'),
|
||||
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'));
|
||||
|
||||
// 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
|
||||
await Promise.race([
|
||||
new Promise<void>((resolve) => {
|
||||
runPersistentCommand('npx', ['-y', '--quiet', '--prefer-online', 'n8n@next'], {
|
||||
cwd: n8nUserFolder,
|
||||
env: {
|
||||
...process.env,
|
||||
N8N_DEV_RELOAD: 'true',
|
||||
N8N_RUNNERS_ENABLED: 'true',
|
||||
DB_SQLITE_POOL_SIZE: '10',
|
||||
N8N_USER_FOLDER: n8nUserFolder,
|
||||
N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS: 'false',
|
||||
},
|
||||
name: 'n8n',
|
||||
color: picocolors.green,
|
||||
allowOutput: (line) => {
|
||||
if (line.includes('Initializing n8n process')) {
|
||||
resolve();
|
||||
}
|
||||
try {
|
||||
await Promise.race([
|
||||
new Promise<void>((resolve) => {
|
||||
runPersistentCommand('npx', ['-y', '--quiet', '--prefer-online', 'n8n@latest'], {
|
||||
cwd: n8nUserFolder,
|
||||
env: {
|
||||
...process.env,
|
||||
N8N_DEV_RELOAD: 'true',
|
||||
N8N_RUNNERS_ENABLED: 'true',
|
||||
DB_SQLITE_POOL_SIZE: '10',
|
||||
N8N_USER_FOLDER: n8nUserFolder,
|
||||
},
|
||||
name: 'n8n',
|
||||
color: picocolors.green,
|
||||
allowOutput: (line) => {
|
||||
if (line.includes('Initializing n8n process')) {
|
||||
resolve();
|
||||
}
|
||||
|
||||
return setupComplete;
|
||||
},
|
||||
});
|
||||
}),
|
||||
new Promise<void>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
const error = new Error('n8n startup timeout after 120 seconds');
|
||||
onCancel(error.message);
|
||||
reject(error);
|
||||
}, 120_000);
|
||||
}),
|
||||
]);
|
||||
return setupComplete;
|
||||
},
|
||||
});
|
||||
}),
|
||||
new Promise<void>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
const error = new Error('n8n startup timeout after 120 seconds');
|
||||
reject(error);
|
||||
}, 120_000);
|
||||
}),
|
||||
]);
|
||||
|
||||
setupComplete = true;
|
||||
npxN8nSpinner.stop('Started n8n dev server');
|
||||
setupComplete = true;
|
||||
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');
|
||||
|
||||
@@ -7,22 +7,100 @@ import { jsonParse } from '../../utils/json';
|
||||
|
||||
export function commands() {
|
||||
const childProcesses: ChildProcess[] = [];
|
||||
let isShuttingDown = false;
|
||||
|
||||
function registerChild(child: ChildProcess) {
|
||||
const registerChild = (child: ChildProcess): void => {
|
||||
childProcesses.push(child);
|
||||
}
|
||||
};
|
||||
|
||||
function cleanup(signal: 'SIGINT' | 'SIGTERM') {
|
||||
for (const child of childProcesses) {
|
||||
const killChild = (child: ChildProcess, signal: NodeJS.Signals): void => {
|
||||
if (!child.killed) {
|
||||
child.kill(signal);
|
||||
}
|
||||
process.exit();
|
||||
}
|
||||
};
|
||||
|
||||
process.on('SIGINT', () => cleanup('SIGINT'));
|
||||
process.on('SIGTERM', () => cleanup('SIGTERM'));
|
||||
const forceKillAllChildren = (): void => {
|
||||
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,
|
||||
args: string[],
|
||||
opts: {
|
||||
@@ -32,7 +110,7 @@ export function commands() {
|
||||
color?: Formatter;
|
||||
allowOutput?: (line: string) => boolean;
|
||||
} = {},
|
||||
) {
|
||||
): ChildProcess => {
|
||||
const child = spawn(cmd, args, {
|
||||
cwd: opts.cwd,
|
||||
env: { ...process.env, ...opts.env },
|
||||
@@ -41,46 +119,21 @@ export function commands() {
|
||||
|
||||
registerChild(child);
|
||||
|
||||
function stripClearCodes(input: string): string {
|
||||
// Remove clear screen/reset ANSI codes
|
||||
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 logger = createLogger(opts.name, opts.color, opts.allowOutput);
|
||||
const handleOutput = (data: Buffer) => processOutput(data, logger);
|
||||
|
||||
const log = (text: string) => {
|
||||
if (opts.allowOutput && !opts.allowOutput(text)) return;
|
||||
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.stdout?.on('data', handleOutput);
|
||||
child.stderr?.on('data', handleOutput);
|
||||
|
||||
child.on('close', (code) => {
|
||||
console.log(`${opts.name ?? cmd} exited with code ${code}`);
|
||||
process.exit(code);
|
||||
if (!isShuttingDown) {
|
||||
console.log(`${opts.name ?? cmd} exited with code ${code}`);
|
||||
process.exit(code ?? 0);
|
||||
}
|
||||
});
|
||||
|
||||
return child;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
runPersistentCommand,
|
||||
|
||||
@@ -7,10 +7,10 @@ import { validateNodeName } from '../../utils/validation';
|
||||
export const nodeNamePrompt = async () =>
|
||||
await withCancelHandler(
|
||||
text({
|
||||
message: 'What is your node called?',
|
||||
placeholder: 'n8n-nodes-example',
|
||||
message: "Package name (must start with 'n8n-nodes-' or '@org/n8n-nodes-')",
|
||||
placeholder: 'n8n-nodes-my-app',
|
||||
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]+)*)$/;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user