Add Activepieces integration for workflow automation
- Add Activepieces fork with SmoothSchedule custom piece - Create integrations app with Activepieces service layer - Add embed token endpoint for iframe integration - Create Automations page with embedded workflow builder - Add sidebar visibility fix for embed mode - Add list inactive customers endpoint to Public API - Include SmoothSchedule triggers: event created/updated/cancelled - Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
44
activepieces-fork/packages/cli/src/index.ts
Normal file
44
activepieces-fork/packages/cli/src/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Command } from 'commander';
|
||||
import { createActionCommand } from './lib/commands/create-action';
|
||||
import { createPieceCommand } from './lib/commands/create-piece';
|
||||
import { createTriggerCommand } from './lib/commands/create-trigger';
|
||||
import { syncPieceCommand } from './lib/commands/sync-pieces';
|
||||
import { publishPieceCommand } from './lib/commands/publish-piece';
|
||||
import { buildPieceCommand } from './lib/commands/build-piece';
|
||||
import { generateWorkerTokenCommand } from './lib/commands/generate-worker-token';
|
||||
import { generateTranslationFileForAllPiecesCommand, generateTranslationFileForPieceCommand } from './lib/commands/generate-translation-file-for-piece';
|
||||
|
||||
const pieceCommand = new Command('pieces')
|
||||
.description('Manage pieces');
|
||||
|
||||
pieceCommand.addCommand(createPieceCommand);
|
||||
pieceCommand.addCommand(syncPieceCommand);
|
||||
pieceCommand.addCommand(publishPieceCommand);
|
||||
pieceCommand.addCommand(buildPieceCommand);
|
||||
pieceCommand.addCommand(generateTranslationFileForPieceCommand);
|
||||
pieceCommand.addCommand(generateTranslationFileForAllPiecesCommand);
|
||||
const actionCommand = new Command('actions')
|
||||
.description('Manage actions');
|
||||
|
||||
actionCommand.addCommand(createActionCommand);
|
||||
|
||||
const triggerCommand = new Command('triggers')
|
||||
.description('Manage triggers')
|
||||
|
||||
triggerCommand.addCommand(createTriggerCommand)
|
||||
|
||||
|
||||
const workerCommand = new Command('workers')
|
||||
.description('Manage workers')
|
||||
|
||||
workerCommand.addCommand(generateWorkerTokenCommand)
|
||||
|
||||
const program = new Command();
|
||||
|
||||
program.version('0.0.1').description('Activepieces CLI');
|
||||
|
||||
program.addCommand(pieceCommand);
|
||||
program.addCommand(actionCommand);
|
||||
program.addCommand(triggerCommand);
|
||||
program.addCommand(workerCommand);
|
||||
program.parse(process.argv);
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Command } from "commander";
|
||||
import { buildPiece, findPiece } from '../utils/piece-utils';
|
||||
import chalk from "chalk";
|
||||
import inquirer from "inquirer";
|
||||
|
||||
async function buildPieces(pieceName: string) {
|
||||
const pieceFolder = await findPiece(pieceName);
|
||||
const { outputFolder } = await buildPiece(pieceFolder);
|
||||
console.info(chalk.green(`Piece '${pieceName}' built and packed successfully at ${outputFolder}.`));
|
||||
}
|
||||
|
||||
export const buildPieceCommand = new Command('build')
|
||||
.description('Build pieces without publishing')
|
||||
.argument('[name]', 'name of the piece to build')
|
||||
.action(async (name) => {
|
||||
const questions = [
|
||||
{
|
||||
type: 'input',
|
||||
name: 'name',
|
||||
message: 'Enter the piece folder name',
|
||||
placeholder: 'google-drive',
|
||||
when() {
|
||||
return !name
|
||||
}
|
||||
},
|
||||
];
|
||||
const answers = await inquirer.prompt(questions);
|
||||
await buildPieces(name ? name : answers.name);
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'commander';
|
||||
import inquirer from 'inquirer';
|
||||
import { assertPieceExists, displayNameToCamelCase, displayNameToKebabCase, findPiece } from '../utils/piece-utils';
|
||||
import { checkIfFileExists, makeFolderRecursive } from '../utils/files';
|
||||
import { join } from 'node:path';
|
||||
|
||||
function createActionTemplate(displayName: string, description: string) {
|
||||
const camelCase = displayNameToCamelCase(displayName)
|
||||
const actionTemplate = `import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
|
||||
export const ${camelCase} = createAction({
|
||||
// auth: check https://www.activepieces.com/docs/developers/piece-reference/authentication,
|
||||
name: '${camelCase}',
|
||||
displayName: '${displayName}',
|
||||
description: '${description}',
|
||||
props: {},
|
||||
async run() {
|
||||
// Action logic here
|
||||
},
|
||||
});
|
||||
`;
|
||||
|
||||
return actionTemplate
|
||||
}
|
||||
|
||||
const checkIfActionExists = async (actionPath: string) => {
|
||||
if (await checkIfFileExists(actionPath)) {
|
||||
console.log(chalk.red(`🚨 Action already exists at ${actionPath}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
const createAction = async (pieceName: string, displayActionName: string, actionDescription: string) => {
|
||||
const actionTemplate = createActionTemplate(displayActionName, actionDescription)
|
||||
const actionName = displayNameToKebabCase(displayActionName)
|
||||
const pieceFolder = await findPiece(pieceName);
|
||||
assertPieceExists(pieceFolder)
|
||||
console.log(chalk.blue(`Piece path: ${pieceFolder}`))
|
||||
const actionsFolder = join(pieceFolder, 'src', 'lib', 'actions')
|
||||
const actionPath = join(actionsFolder, `${actionName}.ts`)
|
||||
await checkIfActionExists(actionPath)
|
||||
|
||||
await makeFolderRecursive(actionsFolder);
|
||||
await writeFile(actionPath, actionTemplate);
|
||||
console.log(chalk.yellow('✨'), `Action ${actionPath} created`);
|
||||
};
|
||||
|
||||
|
||||
export const createActionCommand = new Command('create')
|
||||
.description('Create a new action')
|
||||
.action(async () => {
|
||||
const questions = [
|
||||
{
|
||||
type: 'input',
|
||||
name: 'pieceName',
|
||||
message: 'Enter the piece folder name:',
|
||||
placeholder: 'google-drive',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'actionName',
|
||||
message: 'Enter the action display name',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'actionDescription',
|
||||
message: 'Enter the action description',
|
||||
}
|
||||
];
|
||||
|
||||
const answers = await inquirer.prompt(questions);
|
||||
createAction(answers.pieceName, answers.actionName, answers.actionDescription);
|
||||
});
|
||||
247
activepieces-fork/packages/cli/src/lib/commands/create-piece.ts
Normal file
247
activepieces-fork/packages/cli/src/lib/commands/create-piece.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'commander';
|
||||
import { readdir, unlink, writeFile } from 'fs/promises';
|
||||
import inquirer from 'inquirer';
|
||||
import assert from 'node:assert';
|
||||
import { exec } from '../utils/exec';
|
||||
import {
|
||||
readPackageEslint,
|
||||
readProjectJson,
|
||||
writePackageEslint,
|
||||
writeProjectJson,
|
||||
} from '../utils/files';
|
||||
import { findPiece } from '../utils/piece-utils';
|
||||
|
||||
const validatePieceName = async (pieceName: string) => {
|
||||
console.log(chalk.yellow('Validating piece name....'));
|
||||
const pieceNamePattern = /^(?![._])[a-z0-9-]{1,214}$/;
|
||||
if (!pieceNamePattern.test(pieceName)) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
`🚨 Invalid piece name: ${pieceName}. Piece names can only contain lowercase letters, numbers, and hyphens.`
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
const validatePackageName = async (packageName: string) => {
|
||||
console.log(chalk.yellow('Validating package name....'));
|
||||
const packageNamePattern = /^(?:@[a-zA-Z0-9-]+\/)?[a-zA-Z0-9-]+$/;
|
||||
if (!packageNamePattern.test(packageName)) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
`🚨 Invalid package name: ${packageName}. Package names can only contain lowercase letters, numbers, and hyphens.`
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
const checkIfPieceExists = async (pieceName: string) => {
|
||||
const pieceFolder = await findPiece(pieceName);
|
||||
if (pieceFolder) {
|
||||
console.log(chalk.red(`🚨 Piece already exists at ${pieceFolder}`));
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
const nxGenerateNodeLibrary = async (
|
||||
pieceName: string,
|
||||
packageName: string,
|
||||
pieceType: string
|
||||
) => {
|
||||
const nxGenerateCommand = [
|
||||
`npx nx generate @nx/node:library`,
|
||||
`--directory=packages/pieces/${pieceType}/${pieceName}`,
|
||||
`--name=pieces-${pieceName}`,
|
||||
`--importPath=${packageName}`,
|
||||
'--publishable',
|
||||
'--buildable',
|
||||
'--projectNameAndRootFormat=as-provided',
|
||||
'--strict',
|
||||
'--unitTestRunner=none',
|
||||
].join(' ');
|
||||
|
||||
console.log(chalk.blue(`🛠️ Executing nx command: ${nxGenerateCommand}`));
|
||||
|
||||
await exec(nxGenerateCommand);
|
||||
};
|
||||
|
||||
const removeUnusedFiles = async (pieceName: string, pieceType: string) => {
|
||||
const path = `packages/pieces/${pieceType}/${pieceName}/src/lib/`;
|
||||
const files = await readdir(path);
|
||||
for (const file of files) {
|
||||
await unlink(path + file);
|
||||
}
|
||||
};
|
||||
function capitalizeFirstLetter(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
||||
}
|
||||
const generateIndexTsFile = async (pieceName: string, pieceType: string) => {
|
||||
const pieceNameCamelCase = pieceName
|
||||
.split('-')
|
||||
.map((s, i) => {
|
||||
if (i === 0) {
|
||||
return s;
|
||||
}
|
||||
|
||||
return s[0].toUpperCase() + s.substring(1);
|
||||
})
|
||||
.join('');
|
||||
|
||||
const indexTemplate = `
|
||||
import { createPiece, PieceAuth } from "@activepieces/pieces-framework";
|
||||
|
||||
export const ${pieceNameCamelCase} = createPiece({
|
||||
displayName: "${capitalizeFirstLetter(pieceName)}",
|
||||
auth: PieceAuth.None(),
|
||||
minimumSupportedRelease: '0.36.1',
|
||||
logoUrl: "https://cdn.activepieces.com/pieces/${pieceName}.png",
|
||||
authors: [],
|
||||
actions: [],
|
||||
triggers: [],
|
||||
});
|
||||
`;
|
||||
|
||||
await writeFile(
|
||||
`packages/pieces/${pieceType}/${pieceName}/src/index.ts`,
|
||||
indexTemplate
|
||||
);
|
||||
};
|
||||
const updateProjectJsonConfig = async (
|
||||
pieceName: string,
|
||||
pieceType: string
|
||||
) => {
|
||||
const projectJson = await readProjectJson(
|
||||
`packages/pieces/${pieceType}/${pieceName}`
|
||||
);
|
||||
const i18nAsset = {
|
||||
input: `packages/pieces/${pieceType}/${pieceName}/src/i18n`,
|
||||
output: './src/i18n',
|
||||
glob: '**/!(i18n.json)'
|
||||
}
|
||||
assert(
|
||||
projectJson.targets?.build?.options,
|
||||
'[updateProjectJsonConfig] targets.build.options is required'
|
||||
);
|
||||
|
||||
projectJson.targets.build.dependsOn = ['prebuild', '^build'];
|
||||
projectJson.targets.prebuild = {
|
||||
dependsOn: ['^build'],
|
||||
executor: 'nx:run-commands',
|
||||
options: {
|
||||
cwd: `packages/pieces/${pieceType}/${pieceName}`,
|
||||
command: 'bun install --no-save --silent'
|
||||
}
|
||||
};
|
||||
|
||||
projectJson.targets.build.options.buildableProjectDepsInPackageJsonType =
|
||||
'dependencies';
|
||||
projectJson.targets.build.options.updateBuildableProjectDepsInPackageJson =
|
||||
true;
|
||||
if(projectJson.targets.build.options.assets){
|
||||
projectJson.targets.build.options.assets.push(i18nAsset);
|
||||
}
|
||||
else{
|
||||
projectJson.targets.build.options.assets = [i18nAsset];
|
||||
}
|
||||
|
||||
const lintFilePatterns = projectJson.targets.lint?.options?.lintFilePatterns;
|
||||
|
||||
if (lintFilePatterns) {
|
||||
const patternIndex = lintFilePatterns.findIndex((item) =>
|
||||
item.endsWith('package.json')
|
||||
);
|
||||
if (patternIndex !== -1) lintFilePatterns?.splice(patternIndex, 1);
|
||||
} else {
|
||||
projectJson.targets.lint = {
|
||||
executor: '@nx/eslint:lint',
|
||||
outputs: ['{options.outputFile}'],
|
||||
};
|
||||
}
|
||||
|
||||
await writeProjectJson(
|
||||
`packages/pieces/${pieceType}/${pieceName}`,
|
||||
projectJson
|
||||
);
|
||||
};
|
||||
const addEslintFile = async (pieceName: string, pieceType: string) => {
|
||||
const eslintFile ={
|
||||
"extends": ["../../../../.eslintrc.base.json"],
|
||||
"ignorePatterns": ["!**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.ts", "*.tsx"],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": ["*.js", "*.jsx"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
await writePackageEslint(
|
||||
`packages/pieces/${pieceType}/${pieceName}`,
|
||||
eslintFile
|
||||
);
|
||||
};
|
||||
const setupGeneratedLibrary = async (pieceName: string, pieceType: string) => {
|
||||
await removeUnusedFiles(pieceName, pieceType);
|
||||
await generateIndexTsFile(pieceName, pieceType);
|
||||
await updateProjectJsonConfig(pieceName, pieceType);
|
||||
await addEslintFile(pieceName, pieceType);
|
||||
};
|
||||
|
||||
export const createPiece = async (
|
||||
pieceName: string,
|
||||
packageName: string,
|
||||
pieceType: string
|
||||
) => {
|
||||
await validatePieceName(pieceName);
|
||||
await validatePackageName(packageName);
|
||||
await checkIfPieceExists(pieceName);
|
||||
await nxGenerateNodeLibrary(pieceName, packageName, pieceType);
|
||||
await setupGeneratedLibrary(pieceName, pieceType);
|
||||
console.log(chalk.green('✨ Done!'));
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`The piece has been generated at: packages/pieces/${pieceType}/${pieceName}`
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const createPieceCommand = new Command('create')
|
||||
.description('Create a new piece')
|
||||
.action(async () => {
|
||||
const questions = [
|
||||
{
|
||||
type: 'input',
|
||||
name: 'pieceName',
|
||||
message: 'Enter the piece name:',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'packageName',
|
||||
message: 'Enter the package name:',
|
||||
default: (answers: any) => `@activepieces/piece-${answers.pieceName}`,
|
||||
when: (answers: any) => answers.pieceName !== undefined,
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
name: 'pieceType',
|
||||
message: 'Select the piece type:',
|
||||
choices: ['community', 'custom'],
|
||||
default: 'community',
|
||||
},
|
||||
];
|
||||
|
||||
const answers = await inquirer.prompt(questions);
|
||||
createPiece(answers.pieceName, answers.packageName, answers.pieceType);
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'commander';
|
||||
import inquirer from 'inquirer';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { checkIfFileExists, makeFolderRecursive } from '../utils/files';
|
||||
import {
|
||||
assertPieceExists,
|
||||
displayNameToCamelCase,
|
||||
displayNameToKebabCase, findPiece,
|
||||
} from '../utils/piece-utils';
|
||||
|
||||
function createTriggerTemplate(displayName: string, description: string, technique: string) {
|
||||
const camelCase = displayNameToCamelCase(displayName)
|
||||
let triggerTemplate = ''
|
||||
if (technique === 'polling') {
|
||||
triggerTemplate = `
|
||||
import { createTrigger, TriggerStrategy, AppConnectionValueForAuthProperty } from '@activepieces/pieces-framework';
|
||||
import { DedupeStrategy, Polling, pollingHelper } from '@activepieces/pieces-common';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// replace auth with piece auth variable
|
||||
const polling: Polling<AppConnectionValueForAuthProperty<undefined>, Record<string, never> > = {
|
||||
strategy: DedupeStrategy.TIMEBASED,
|
||||
items: async ({ propsValue, lastFetchEpochMS }) => {
|
||||
// implement the logic to fetch the items
|
||||
const items = [ {id: 1, created_date: '2021-01-01T00:00:00Z'}, {id: 2, created_date: '2021-01-01T00:00:00Z'}];
|
||||
return items.map((item) => ({
|
||||
epochMilliSeconds: dayjs(item.created_date).valueOf(),
|
||||
data: item,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export const ${camelCase} = createTrigger({
|
||||
// auth: check https://www.activepieces.com/docs/developers/piece-reference/authentication,
|
||||
name: '${camelCase}',
|
||||
displayName: '${displayName}',
|
||||
description: '${description}',
|
||||
props: {},
|
||||
sampleData: {},
|
||||
type: TriggerStrategy.POLLING,
|
||||
async test(context) {
|
||||
return await pollingHelper.test(polling, context);
|
||||
},
|
||||
async onEnable(context) {
|
||||
const { store, auth, propsValue } = context;
|
||||
await pollingHelper.onEnable(polling, { store, auth, propsValue });
|
||||
},
|
||||
|
||||
async onDisable(context) {
|
||||
const { store, auth, propsValue } = context;
|
||||
await pollingHelper.onDisable(polling, { store, auth, propsValue });
|
||||
},
|
||||
|
||||
async run(context) {
|
||||
return await pollingHelper.poll(polling, context);
|
||||
},
|
||||
});`;
|
||||
}
|
||||
else {
|
||||
triggerTemplate = `
|
||||
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
|
||||
export const ${camelCase} = createTrigger({
|
||||
// auth: check https://www.activepieces.com/docs/developers/piece-reference/authentication,
|
||||
name: '${camelCase}',
|
||||
displayName: '${displayName}',
|
||||
description: '${description}',
|
||||
props: {},
|
||||
sampleData: {},
|
||||
type: TriggerStrategy.WEBHOOK,
|
||||
async onEnable(context){
|
||||
// implement webhook creation logic
|
||||
},
|
||||
async onDisable(context){
|
||||
// implement webhook deletion logic
|
||||
},
|
||||
async run(context){
|
||||
return [context.payload.body]
|
||||
}
|
||||
})`;
|
||||
|
||||
}
|
||||
|
||||
return triggerTemplate
|
||||
}
|
||||
const checkIfTriggerExists = async (triggerPath: string) => {
|
||||
if (await checkIfFileExists(triggerPath)) {
|
||||
console.log(chalk.red(`🚨 Trigger already exists at ${triggerPath}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
const createTrigger = async (pieceName: string, displayTriggerName: string, triggerDescription: string, triggerTechnique: string) => {
|
||||
const triggerTemplate = createTriggerTemplate(displayTriggerName, triggerDescription, triggerTechnique)
|
||||
const triggerName = displayNameToKebabCase(displayTriggerName)
|
||||
const pieceFolder = await findPiece(pieceName);
|
||||
assertPieceExists(pieceFolder)
|
||||
console.log(chalk.blue(`Piece path: ${pieceFolder}`))
|
||||
|
||||
const triggersFolder = join(pieceFolder, 'src', 'lib', 'triggers')
|
||||
const triggerPath = join(triggersFolder, `${triggerName}.ts`)
|
||||
await checkIfTriggerExists(triggerPath)
|
||||
|
||||
await makeFolderRecursive(triggersFolder);
|
||||
await writeFile(triggerPath, triggerTemplate);
|
||||
console.log(chalk.yellow('✨'), `Trigger ${triggerPath} created`);
|
||||
};
|
||||
|
||||
|
||||
export const createTriggerCommand = new Command('create')
|
||||
.description('Create a new trigger')
|
||||
.action(async () => {
|
||||
const questions = [
|
||||
{
|
||||
type: 'input',
|
||||
name: 'pieceName',
|
||||
message: 'Enter the piece folder name:',
|
||||
placeholder: 'google-drive',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'triggerName',
|
||||
message: 'Enter the trigger display name:',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'triggerDescription',
|
||||
message: 'Enter the trigger description:',
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
name: 'triggerTechnique',
|
||||
message: 'Select the trigger technique:',
|
||||
choices: ['polling', 'webhook'],
|
||||
default: 'webhook',
|
||||
},
|
||||
];
|
||||
|
||||
const answers = await inquirer.prompt(questions);
|
||||
createTrigger(answers.pieceName, answers.triggerName, answers.triggerDescription, answers.triggerTechnique);
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'commander';
|
||||
import { buildPiece, findPiece, findPieces } from '../utils/piece-utils';
|
||||
import { makeFolderRecursive } from '../utils/files';
|
||||
import { join, basename } from 'node:path';
|
||||
import { exec } from '../utils/exec';
|
||||
import { pieceTranslation } from '@activepieces/pieces-framework';
|
||||
import { MAX_KEY_LENGTH_FOR_CORWDIN } from '@activepieces/shared';
|
||||
|
||||
const findPieceInModule= async (pieceOutputFile: string) => {
|
||||
const module = await import(pieceOutputFile);
|
||||
const exports = Object.values(module);
|
||||
for (const e of exports) {
|
||||
if (e !== null && e !== undefined && e.constructor.name === 'Piece') {
|
||||
return e
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Piece not found in module, please check the piece output file ${pieceOutputFile}`);
|
||||
}
|
||||
|
||||
const installDependencies = async (pieceFolder: string) => {
|
||||
console.log(chalk.blue(`Installing dependencies ${pieceFolder}`))
|
||||
await exec(`bun install`, {cwd: pieceFolder,})
|
||||
console.log(chalk.green(`Dependencies installed ${pieceFolder}`))
|
||||
}
|
||||
|
||||
|
||||
function getPropertyValue(object: Record<string, unknown>, path: string): unknown {
|
||||
const parsedKeys = path.split('.');
|
||||
if (parsedKeys[0] === '*') {
|
||||
return Object.values(object).map(item => getPropertyValue(item as Record<string, unknown>, parsedKeys.slice(1).join('.'))).filter(Boolean).flat()
|
||||
}
|
||||
const nextObject = object[parsedKeys[0]] as Record<string, unknown>;
|
||||
if (nextObject && parsedKeys.length > 1) {
|
||||
return getPropertyValue(nextObject, parsedKeys.slice(1).join('.'));
|
||||
}
|
||||
return nextObject;
|
||||
}
|
||||
|
||||
const generateTranslationFileFromPiece = (piece: Record<string, unknown>) => { const translation: Record<string, string> = {}
|
||||
try {
|
||||
pieceTranslation.pathsToValuesToTranslate.forEach(path => {
|
||||
const value = getPropertyValue(piece, path)
|
||||
if (value) {
|
||||
if (typeof value === 'string') {
|
||||
translation[value.slice(0, MAX_KEY_LENGTH_FOR_CORWDIN)] = value
|
||||
}
|
||||
else if (Array.isArray(value)) {
|
||||
value.forEach(item => {
|
||||
translation[item.slice(0, MAX_KEY_LENGTH_FOR_CORWDIN)] = item
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
catch (err) {
|
||||
console.error(`error generating translation file for piece ${piece.name}:`, err)
|
||||
}
|
||||
|
||||
return translation
|
||||
}
|
||||
|
||||
|
||||
|
||||
const generateTranslationFile = async (pieceName: string) => {
|
||||
const pieceRoot = await findPiece(pieceName)
|
||||
await buildPiece(pieceRoot)
|
||||
const outputFolder = pieceRoot.replace('packages/', 'dist/packages/')
|
||||
try{
|
||||
await installDependencies(outputFolder)
|
||||
const pieceFromModule = await findPieceInModule(outputFolder);
|
||||
const i18n = generateTranslationFileFromPiece({actions: (pieceFromModule as any)._actions, triggers: (pieceFromModule as any)._triggers, description: (pieceFromModule as any).description, displayName: (pieceFromModule as any).displayName, auth: (pieceFromModule as any).auth});
|
||||
const i18nFolder = join(pieceRoot, 'src', 'i18n')
|
||||
await makeFolderRecursive(i18nFolder);
|
||||
await writeFile(join(i18nFolder, 'translation.json'), JSON.stringify(i18n, null, 2));
|
||||
console.log(chalk.yellow('✨'), `Translation file for piece created in ${i18nFolder}`);
|
||||
} catch (error) {
|
||||
console.error(chalk.red('❌'), `Error generating translation file for piece ${pieceName}, make sure you built the piece`,error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const generateTranslationFileForPieceCommand = new Command('generate-translation-file')
|
||||
.description('Generate i18n for a piece')
|
||||
.argument('<pieceName>', 'The name of the piece to generate i18n for')
|
||||
.action(async (pieceName: string) => {
|
||||
await generateTranslationFile(pieceName);
|
||||
});
|
||||
export const generateTranslationFileForAllPiecesCommand = new Command('generate-translation-file-for-all-pieces')
|
||||
.description('Generate i18n for all pieces')
|
||||
.requiredOption('--shard-index <shardIndex>', 'Zero-based shard index to process', (value) => parseInt(value, 10))
|
||||
.requiredOption('--shard-total <shardTotal>', 'Total number of shards', (value) => parseInt(value, 10))
|
||||
.action(async ({shardIndex, shardTotal}: { shardIndex: number; shardTotal: number }) => {
|
||||
const piecesDirectory = join(process.cwd(), 'packages', 'pieces', 'community')
|
||||
const pieces = (await findPieces(piecesDirectory)).map(piece => piece.split('/').pop());
|
||||
let totalTime = 0
|
||||
let indexAcrossAllPieces = 0
|
||||
for (const piece of pieces) {
|
||||
if ((indexAcrossAllPieces % shardTotal) !== shardIndex) {
|
||||
indexAcrossAllPieces++
|
||||
continue
|
||||
}
|
||||
const time= performance.now()
|
||||
await generateTranslationFile(piece);
|
||||
console.log(chalk.yellow('✨'), `Translation file for piece ${piece} created in ${(performance.now() - time)/1000}s`)
|
||||
totalTime += (performance.now() - time)/1000
|
||||
indexAcrossAllPieces++
|
||||
}
|
||||
console.log(chalk.yellow('✨'), `Total time taken to generate translation files for selected pieces: ${totalTime}s`)
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import { prompt } from 'inquirer';
|
||||
import { nanoid } from 'nanoid';
|
||||
import jwtLibrary from 'jsonwebtoken';
|
||||
|
||||
const KEY_ID = '1'
|
||||
const ISSUER = 'activepieces'
|
||||
const ALGORITHM = 'HS256'
|
||||
|
||||
export const generateWorkerTokenCommand = new Command('token')
|
||||
.description('Generate a JWT token for worker authentication')
|
||||
.action(async () => {
|
||||
const answers = await prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'jwtSecret',
|
||||
message: 'Enter your JWT secret (should be the same as AP_JWT_SECRET used for the app server):',
|
||||
validate: (input) => {
|
||||
if (!input) {
|
||||
return 'JWT secret is required';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
const payload = {
|
||||
id: nanoid(),
|
||||
type: 'WORKER',
|
||||
};
|
||||
|
||||
// 100 years in seconds
|
||||
const expiresIn = 100 * 365 * 24 * 60 * 60;
|
||||
|
||||
try {
|
||||
const token = jwtLibrary.sign(payload, answers.jwtSecret, {
|
||||
expiresIn,
|
||||
keyid: KEY_ID,
|
||||
algorithm: ALGORITHM,
|
||||
issuer: ISSUER,
|
||||
});
|
||||
console.log(chalk.green('\nGenerated Worker Token, Please use it in AP_WORKER_TOKEN environment variable:'));
|
||||
console.log(chalk.yellow(token));
|
||||
|
||||
} catch (error) {
|
||||
console.error(chalk.red('Failed to generate token:'), error);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Command } from "commander";
|
||||
import { publishPieceFromFolder, findPiece, assertPieceExists } from '../utils/piece-utils';
|
||||
import chalk from "chalk";
|
||||
import inquirer from 'inquirer';
|
||||
import * as dotenv from 'dotenv';
|
||||
|
||||
dotenv.config({path: 'packages/server/api/.env'});
|
||||
|
||||
async function publishPiece(
|
||||
{apiUrl, apiKey, pieceName, failOnError}:
|
||||
{apiUrl: string,
|
||||
apiKey: string,
|
||||
pieceName: string,
|
||||
failOnError: boolean,}
|
||||
) {
|
||||
const pieceFolder = await findPiece(pieceName);
|
||||
assertPieceExists(pieceFolder)
|
||||
await publishPieceFromFolder({
|
||||
pieceFolder,
|
||||
apiUrl,
|
||||
apiKey,
|
||||
failOnError
|
||||
});
|
||||
}
|
||||
|
||||
function assertNullOrUndefinedOrEmpty(value: any, message: string) {
|
||||
if (value === undefined || value === null || (typeof value === 'string' && value.trim() === '')) {
|
||||
console.error(chalk.red(message));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export const publishPieceCommand = new Command('publish')
|
||||
.description('Publish pieces to the platform')
|
||||
.option('-f, --fail-on-error', 'Exit the process if an error occurs while syncing a piece', false)
|
||||
.action(async (command) => {
|
||||
const questions = [
|
||||
{
|
||||
type: 'input',
|
||||
name: 'name',
|
||||
message: 'Enter the piece folder name',
|
||||
placeholder: 'google-drive',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'apiUrl',
|
||||
message: 'Enter the API URL',
|
||||
placeholder: 'https://cloud.activepieces.com/api',
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
name: 'apiKeySource',
|
||||
message: 'Select the API Key source',
|
||||
choices: ['Env Variable (AP_API_KEY)', 'Manually'],
|
||||
default: 'Env Variable (AP_API_KEY)'
|
||||
}
|
||||
]
|
||||
|
||||
const answers = await inquirer.prompt(questions);
|
||||
if (answers.apiKeySource === 'Manually') {
|
||||
const apiKeyAnswers = await inquirer.prompt([{
|
||||
type: 'input',
|
||||
name: 'apiKey',
|
||||
message: 'Enter the API Key',
|
||||
}]);
|
||||
answers.apiKey = apiKeyAnswers.apiKey;
|
||||
}
|
||||
const apiKey = answers.apiKeySource === 'Env Variable (AP_API_KEY)' ? process.env.AP_API_KEY : answers.apiKey;
|
||||
assertNullOrUndefinedOrEmpty(answers.name, 'Piece name is required');
|
||||
assertNullOrUndefinedOrEmpty(answers.apiUrl, 'API URL is required');
|
||||
assertNullOrUndefinedOrEmpty(apiKey, 'API Key is required');
|
||||
const apiUrlWithoutTrailSlash = answers.apiUrl.replace(/\/$/, '');
|
||||
const { failOnError } = command;
|
||||
|
||||
await publishPiece({
|
||||
apiUrl: apiUrlWithoutTrailSlash,
|
||||
apiKey,
|
||||
pieceName: answers.name,
|
||||
failOnError
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Command } from "commander";
|
||||
import { findPieces, publishPieceFromFolder } from '../utils/piece-utils';
|
||||
import chalk from "chalk";
|
||||
import { join } from "path";
|
||||
|
||||
async function syncPieces(
|
||||
params:
|
||||
{apiUrl: string,
|
||||
apiKey: string,
|
||||
pieces: string[] | null,
|
||||
failOnError: boolean,}
|
||||
) {
|
||||
const piecesDirectory = join(process.cwd(), 'packages', 'pieces', 'custom')
|
||||
const pieceFolders = await findPieces(piecesDirectory, params.pieces);
|
||||
for (const pieceFolder of pieceFolders) {
|
||||
await publishPieceFromFolder({
|
||||
pieceFolder,
|
||||
...params
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const syncPieceCommand = new Command('sync')
|
||||
.description('Find new pieces versions and sync them with the database')
|
||||
.requiredOption('-h, --apiUrl <url>', 'API URL ex: https://cloud.activepieces.com/api')
|
||||
.option('-p, --pieces <pieces...>', 'Specify one or more piece names to sync. ' +
|
||||
'If not provided, all custom pieces in the directory will be synced.')
|
||||
.option('-f, --fail-on-error', 'Exit the process if an error occurs while syncing a piece', false)
|
||||
.action(async (options) => {
|
||||
const apiKey = process.env.AP_API_KEY;
|
||||
const pieces = options.pieces ? [...new Set<string>(options.pieces)] : null;
|
||||
const failOnError = options.failOnError;
|
||||
if (!apiKey) {
|
||||
console.error(chalk.red('AP_API_KEY environment variable is required'));
|
||||
process.exit(1);
|
||||
}
|
||||
await syncPieces({
|
||||
apiUrl: options.apiUrl.replace(/\/$/, ''),
|
||||
apiKey,
|
||||
pieces,
|
||||
failOnError
|
||||
});
|
||||
});
|
||||
4
activepieces-fork/packages/cli/src/lib/utils/exec.ts
Normal file
4
activepieces-fork/packages/cli/src/lib/utils/exec.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { exec as execCallback } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
export const exec = promisify(execCallback);
|
||||
97
activepieces-fork/packages/cli/src/lib/utils/files.ts
Normal file
97
activepieces-fork/packages/cli/src/lib/utils/files.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
constants,
|
||||
readFile,
|
||||
access,
|
||||
writeFile,
|
||||
mkdir,
|
||||
} from 'node:fs/promises';
|
||||
|
||||
export type PackageJson = {
|
||||
name: string;
|
||||
version: string;
|
||||
keywords: string[];
|
||||
};
|
||||
|
||||
export type ProjectJson = {
|
||||
name: string;
|
||||
targets?: {
|
||||
prebuild?: {
|
||||
executor: string;
|
||||
dependsOn?: string[];
|
||||
options: {
|
||||
cwd: string;
|
||||
command: string;
|
||||
};
|
||||
};
|
||||
build?: {
|
||||
dependsOn?: string[];
|
||||
options?: {
|
||||
buildableProjectDepsInPackageJsonType?:
|
||||
| 'peerDependencies'
|
||||
| 'dependencies';
|
||||
updateBuildableProjectDepsInPackageJson: boolean;
|
||||
assets?: ({
|
||||
input: string;
|
||||
output: string;
|
||||
glob: string;
|
||||
} | string)[];
|
||||
};
|
||||
};
|
||||
lint: {
|
||||
executor: string;
|
||||
outputs: string[];
|
||||
options?: {
|
||||
lintFilePatterns: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const checkIfFileExists = async (filePath: string) => {
|
||||
try {
|
||||
await access(filePath, constants.F_OK);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const readJsonFile = async <T>(path: string): Promise<T> => {
|
||||
const jsonFile = await readFile(path, { encoding: 'utf-8' });
|
||||
return JSON.parse(jsonFile) as T;
|
||||
};
|
||||
|
||||
const writeJsonFile = async (path: string, data: unknown): Promise<void> => {
|
||||
const serializedData = JSON.stringify(data, null, 2);
|
||||
await writeFile(path, serializedData, { encoding: 'utf-8' });
|
||||
};
|
||||
|
||||
export const readPackageJson = async (path: string): Promise<PackageJson> => {
|
||||
return await readJsonFile(`${path}/package.json`);
|
||||
};
|
||||
|
||||
export const readProjectJson = async (path: string): Promise<ProjectJson> => {
|
||||
return await readJsonFile(`${path}/project.json`);
|
||||
};
|
||||
|
||||
export const readPackageEslint = async (path: string): Promise<any> => {
|
||||
return await readJsonFile(`${path}/.eslintrc.json`);
|
||||
};
|
||||
|
||||
export const writePackageEslint = async (
|
||||
path: string,
|
||||
eslint: any
|
||||
): Promise<void> => {
|
||||
return await writeJsonFile(`${path}/.eslintrc.json`, eslint);
|
||||
};
|
||||
|
||||
export const writeProjectJson = async (
|
||||
path: string,
|
||||
projectJson: ProjectJson
|
||||
): Promise<void> => {
|
||||
return await writeJsonFile(`${path}/project.json`, projectJson);
|
||||
};
|
||||
|
||||
export const makeFolderRecursive = async (path: string): Promise<void> => {
|
||||
await mkdir(path, { recursive: true });
|
||||
};
|
||||
176
activepieces-fork/packages/cli/src/lib/utils/piece-utils.ts
Normal file
176
activepieces-fork/packages/cli/src/lib/utils/piece-utils.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { readdir, stat } from 'node:fs/promises'
|
||||
import * as path from 'path'
|
||||
import { cwd } from 'node:process'
|
||||
import { readPackageJson, readProjectJson } from './files'
|
||||
import { exec } from './exec'
|
||||
import axios from 'axios'
|
||||
import chalk from 'chalk'
|
||||
import FormData from 'form-data';
|
||||
import fs from 'fs';
|
||||
|
||||
export const piecesPath = () => path.join(cwd(), 'packages', 'pieces')
|
||||
export const customPiecePath = () => path.join(piecesPath(), 'custom')
|
||||
|
||||
/**
|
||||
* Finds and returns the paths of specific pieces or all available pieces in a given directory.
|
||||
*
|
||||
* @param inputPath - The root directory to search for pieces. If not provided, a default path to custom pieces is used.
|
||||
* @param pieces - An optional array of piece names to search for. If not provided, all pieces in the directory are returned.
|
||||
* @returns A promise resolving to an array of strings representing the paths of the found pieces.
|
||||
*/
|
||||
export async function findPieces(inputPath?: string, pieces?: string[]): Promise<string[]> {
|
||||
const piecesPath = inputPath ?? customPiecePath()
|
||||
const piecesFolders = await traverseFolder(piecesPath)
|
||||
if (pieces) {
|
||||
return pieces.flatMap((piece) => {
|
||||
const folder = piecesFolders.find((p) => {
|
||||
const normalizedPath = path.normalize(p);
|
||||
return normalizedPath.endsWith(path.sep + piece);
|
||||
});
|
||||
if (!folder) {
|
||||
return [];
|
||||
}
|
||||
return [folder];
|
||||
});
|
||||
} else {
|
||||
return piecesFolders
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns the path of a single piece. Exits the process if the piece is not found.
|
||||
*
|
||||
* @param pieceName - The name of the piece to search for.
|
||||
* @returns A promise resolving to a string representing the path of the found piece. If not found, the process exits.
|
||||
*/
|
||||
export async function findPiece(pieceName: string): Promise<string | null> {
|
||||
return (await findPieces(piecesPath(), [pieceName]))[0] ?? null;
|
||||
}
|
||||
|
||||
export async function buildPiece(pieceFolder: string): Promise<{ outputFolder: string, outputFile: string }> {
|
||||
const projectJson = await readProjectJson(pieceFolder);
|
||||
|
||||
await buildPackage(projectJson.name);
|
||||
|
||||
const compiledPath = `dist/packages/${removeStartingSlashes(pieceFolder).split(path.sep + 'packages')[1]}`;
|
||||
|
||||
const { stdout } = await exec('npm pack --json', { cwd: compiledPath });
|
||||
const tarFileName = JSON.parse(stdout)[0].filename;
|
||||
return {
|
||||
outputFolder: compiledPath,
|
||||
outputFile: path.join(compiledPath, tarFileName)
|
||||
};
|
||||
}
|
||||
|
||||
export async function buildPackage(projectName:string) {
|
||||
await exec(`npx nx build ${projectName} --skip-cache`);
|
||||
return {
|
||||
outputFolder: `dist/packages/${projectName}`,
|
||||
}
|
||||
}
|
||||
|
||||
export async function publishPieceFromFolder(
|
||||
{pieceFolder, apiUrl, apiKey, failOnError}:
|
||||
{pieceFolder: string,
|
||||
apiUrl: string,
|
||||
apiKey: string,
|
||||
failOnError: boolean,}
|
||||
) {
|
||||
const projectJson = await readProjectJson(pieceFolder);
|
||||
const packageJson = await readPackageJson(pieceFolder);
|
||||
|
||||
await buildPackage(projectJson.name);
|
||||
|
||||
const { outputFile } = await buildPiece(pieceFolder);
|
||||
const formData = new FormData();
|
||||
|
||||
console.log(chalk.blue(`Uploading ${outputFile}`));
|
||||
formData.append('pieceArchive', fs.createReadStream(outputFile));
|
||||
formData.append('pieceName', packageJson.name);
|
||||
formData.append('pieceVersion', packageJson.version);
|
||||
formData.append('packageType', 'ARCHIVE');
|
||||
formData.append('scope', 'PLATFORM');
|
||||
|
||||
try {
|
||||
await axios.post(`${apiUrl}/v1/pieces`, formData, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
...formData.getHeaders()
|
||||
}
|
||||
});
|
||||
console.info(chalk.green(`Piece '${packageJson.name}' published.`));
|
||||
} catch (error) {
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response?.status === 409) {
|
||||
console.info(chalk.yellow(`Piece '${packageJson.name}' and '${packageJson.version}' already published.`));
|
||||
} else if (error.response && Math.floor(error.response.status / 100) !== 2) {
|
||||
console.info(chalk.red(`Error publishing piece '${packageJson.name}', ${error}` ));
|
||||
if (failOnError) {
|
||||
console.info(chalk.yellow(`Terminating process due to publish failure for piece '${packageJson.name}' (fail-on-error is enabled)`));
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.error(chalk.red(`Unexpected error: ${error.message}`));
|
||||
if (failOnError) {
|
||||
console.info(chalk.yellow(`Terminating process due to unexpected error for piece '${packageJson.name}' (fail-on-error is enabled)`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error(chalk.red(`Unexpected error: ${error.message}`));
|
||||
if (failOnError) {
|
||||
console.info(chalk.yellow(`Terminating process due to unexpected error for piece '${packageJson.name}' (fail-on-error is enabled)`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
async function traverseFolder(folderPath: string): Promise<string[]> {
|
||||
const paths: string[] = []
|
||||
const directoryExists = await stat(folderPath).catch(() => null)
|
||||
|
||||
if (directoryExists && directoryExists.isDirectory()) {
|
||||
const files = await readdir(folderPath)
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(folderPath, file)
|
||||
const fileStats = await stat(filePath)
|
||||
if (fileStats.isDirectory() && file !== 'node_modules' && file !== 'dist') {
|
||||
paths.push(...await traverseFolder(filePath))
|
||||
}
|
||||
else if (file === 'package.json') {
|
||||
paths.push(folderPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
export function displayNameToKebabCase(displayName: string): string {
|
||||
return displayName.toLowerCase().replace(/\s+/g, '-');
|
||||
}
|
||||
|
||||
export function displayNameToCamelCase(input: string): string {
|
||||
const words = input.split(' ');
|
||||
const camelCaseWords = words.map((word, index) => {
|
||||
if (index === 0) {
|
||||
return word.toLowerCase();
|
||||
} else {
|
||||
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
||||
}
|
||||
});
|
||||
return camelCaseWords.join('');
|
||||
}
|
||||
|
||||
export const assertPieceExists = async (pieceName: string | null) => {
|
||||
if (!pieceName) {
|
||||
console.error(chalk.red(`🚨 Piece ${pieceName} not found`));
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const removeStartingSlashes = (str: string) => {
|
||||
return str.startsWith('/') ? str.slice(1) : str;
|
||||
}
|
||||
Reference in New Issue
Block a user