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:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View 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);

View File

@@ -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);
});

View File

@@ -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);
});

View 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);
});

View File

@@ -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);
});

View File

@@ -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`)
});

View File

@@ -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);
}
});

View File

@@ -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
});
});

View File

@@ -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
});
});

View File

@@ -0,0 +1,4 @@
import { exec as execCallback } from 'node:child_process';
import { promisify } from 'node:util';
export const exec = promisify(execCallback);

View 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 });
};

View 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;
}