first commit
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
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
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.executeCommand",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"details": "Execute command allows you to run terminal commands on the computer/server hosting your n8n instance. Useful for executing a shell script or interacting with your n8n instance programmatically via the CLI.",
|
||||
"categories": ["Development", "Core Nodes"],
|
||||
"resources": {
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.executecommand/"
|
||||
}
|
||||
],
|
||||
"generic": [
|
||||
{
|
||||
"label": "How uProc scraped a multi-page website with a low-code workflow",
|
||||
"icon": " 🕸️",
|
||||
"url": "https://n8n.io/blog/how-uproc-scraped-a-multi-page-website-with-a-low-code-workflow/"
|
||||
},
|
||||
{
|
||||
"label": "Why this Product Manager loves workflow automation with n8n",
|
||||
"icon": "🧠",
|
||||
"url": "https://n8n.io/blog/why-this-product-manager-loves-workflow-automation-with-n8n/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"alias": ["Shell", "Command", "OS", "Bash"],
|
||||
"subcategories": {
|
||||
"Core Nodes": ["Helpers"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { exec } from 'child_process';
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
export interface IExecReturnData {
|
||||
exitCode: number;
|
||||
error?: Error;
|
||||
stderr: string;
|
||||
stdout: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Promisifiy exec manually to also get the exit code
|
||||
*
|
||||
*/
|
||||
async function execPromise(command: string): Promise<IExecReturnData> {
|
||||
const returnData: IExecReturnData = {
|
||||
error: undefined,
|
||||
exitCode: 0,
|
||||
stderr: '',
|
||||
stdout: '',
|
||||
};
|
||||
|
||||
return await new Promise((resolve, _reject) => {
|
||||
exec(command, { cwd: process.cwd() }, (error, stdout, stderr) => {
|
||||
returnData.stdout = stdout.trim();
|
||||
returnData.stderr = stderr.trim();
|
||||
|
||||
if (error) {
|
||||
returnData.error = error;
|
||||
}
|
||||
|
||||
resolve(returnData);
|
||||
}).on('exit', (code) => {
|
||||
returnData.exitCode = code || 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export class ExecuteCommand implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Execute Command',
|
||||
name: 'executeCommand',
|
||||
icon: 'fa:terminal',
|
||||
iconColor: 'crimson',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
description: 'Executes a command on the host',
|
||||
defaults: {
|
||||
name: 'Execute Command',
|
||||
color: '#886644',
|
||||
},
|
||||
usableAsTool: true,
|
||||
inputs: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Execute Once',
|
||||
name: 'executeOnce',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Whether to execute only once instead of once for each entry',
|
||||
},
|
||||
{
|
||||
displayName: 'Command',
|
||||
name: 'command',
|
||||
typeOptions: {
|
||||
rows: 5,
|
||||
},
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'echo "test"',
|
||||
description: 'The command to execute',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
let items = this.getInputData();
|
||||
|
||||
let command: string;
|
||||
const executeOnce = this.getNodeParameter('executeOnce', 0) as boolean;
|
||||
|
||||
if (executeOnce) {
|
||||
items = [items[0]];
|
||||
}
|
||||
|
||||
const returnItems: INodeExecutionData[] = [];
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
try {
|
||||
command = this.getNodeParameter('command', itemIndex) as string;
|
||||
|
||||
const { error, exitCode, stdout, stderr } = await execPromise(command);
|
||||
|
||||
if (error !== undefined) {
|
||||
throw new NodeOperationError(this.getNode(), error.message, { itemIndex });
|
||||
}
|
||||
|
||||
returnItems.push({
|
||||
json: {
|
||||
exitCode,
|
||||
stderr,
|
||||
stdout,
|
||||
},
|
||||
pairedItem: {
|
||||
item: itemIndex,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
returnItems.push({
|
||||
json: {
|
||||
error: error.message,
|
||||
},
|
||||
pairedItem: {
|
||||
item: itemIndex,
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return [returnItems];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
|
||||
describe('Execute Execute Command Node', () => {
|
||||
new NodeTestHarness().setupTests();
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
{
|
||||
"name": "Execute Command Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "9f813425-3fe0-49b6-9fc6-a1779bdb71e3",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 420]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"values": {
|
||||
"string": [
|
||||
{
|
||||
"name": "data[0]",
|
||||
"value": "n8n"
|
||||
},
|
||||
{
|
||||
"name": "data[1]",
|
||||
"value": "test"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "cf02f88c-e64d-4831-9d5b-954ad21ca3ae",
|
||||
"name": "Set",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 1,
|
||||
"position": [360, 420]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fieldToSplitOut": "data",
|
||||
"options": {}
|
||||
},
|
||||
"id": "364b8576-cb5d-4ca5-b497-e3437b45831a",
|
||||
"name": "Item Lists",
|
||||
"type": "n8n-nodes-base.itemLists",
|
||||
"typeVersion": 1,
|
||||
"position": [520, 420]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"command": "=echo {{ $json.data }}"
|
||||
},
|
||||
"id": "73854e53-324d-46f8-a222-9a9897807307",
|
||||
"name": "EC Once",
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 1,
|
||||
"position": [700, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"executeOnce": false,
|
||||
"command": "=echo {{ $json.data }}"
|
||||
},
|
||||
"id": "b47ec541-6e85-489f-b925-2f9a77681f18",
|
||||
"name": "EC All",
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 1,
|
||||
"position": [700, 540]
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"EC Once": [
|
||||
{
|
||||
"json": {
|
||||
"exitCode": 0,
|
||||
"stderr": "",
|
||||
"stdout": "n8n"
|
||||
}
|
||||
}
|
||||
],
|
||||
"EC All": [
|
||||
{
|
||||
"json": {
|
||||
"exitCode": 0,
|
||||
"stderr": "",
|
||||
"stdout": "n8n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"exitCode": 0,
|
||||
"stderr": "",
|
||||
"stdout": "test"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Set",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Set": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Item Lists",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Item Lists": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "EC Once",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "EC All",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {},
|
||||
"versionId": "de1a454e-43e9-4c2d-b786-18da5d97940f",
|
||||
"id": "389",
|
||||
"meta": {
|
||||
"instanceId": "REMOVED"
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
Reference in New Issue
Block a user