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:
4
activepieces-fork/tools/scripts/utils/exec.ts
Normal file
4
activepieces-fork/tools/scripts/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)
|
||||
55
activepieces-fork/tools/scripts/utils/files.ts
Normal file
55
activepieces-fork/tools/scripts/utils/files.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
|
||||
export type PackageJson = {
|
||||
name: string
|
||||
version: string
|
||||
keywords: string[]
|
||||
}
|
||||
|
||||
export type ProjectJson = {
|
||||
name: string
|
||||
targets?: {
|
||||
build?: {
|
||||
options?: {
|
||||
buildableProjectDepsInPackageJsonType?: 'peerDependencies' | 'dependencies'
|
||||
updateBuildableProjectDepsInPackageJson: boolean
|
||||
}
|
||||
},
|
||||
lint: {
|
||||
options: {
|
||||
lintFilePatterns: string[]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import assert from 'node:assert'
|
||||
import { ExecException } from 'node:child_process'
|
||||
import axios, { AxiosError } from 'axios'
|
||||
import { exec } from './exec'
|
||||
import { readPackageJson } from './files'
|
||||
|
||||
const getLatestPublishedVersion = async (packageName: string, maxRetries: number = 5): Promise<string | null> => {
|
||||
console.info(`[getLatestPublishedVersion] packageName=${packageName}`);
|
||||
|
||||
const retryDelay = (attempt: number) => Math.pow(4, attempt - 1) * 2000;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
const response = await axios<{ version: string }>(`https://registry.npmjs.org/${packageName}/latest`);
|
||||
const version = response.data.version;
|
||||
console.info(`[getLatestPublishedVersion] packageName=${packageName}, latestVersion=${version}`);
|
||||
return version;
|
||||
} catch (e: unknown) {
|
||||
if (attempt === maxRetries) {
|
||||
throw e; // If it's the last attempt, rethrow the error
|
||||
}
|
||||
|
||||
if (e instanceof AxiosError && e.response?.status === 404) {
|
||||
console.info(`[getLatestPublishedVersion] packageName=${packageName}, latestVersion=null`);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.warn(`[getLatestPublishedVersion] packageName=${packageName}, attempt=${attempt}, error=${e}`);
|
||||
const delay = retryDelay(attempt);
|
||||
await new Promise(resolve => setTimeout(resolve, delay)); // Wait for the delay before retrying
|
||||
}
|
||||
}
|
||||
|
||||
return null; // Return null if all retries fail
|
||||
};
|
||||
|
||||
|
||||
const packageChangedFromMainBranch = async (path: string): Promise<boolean> => {
|
||||
const cleaned = path.includes('/packages') ? `packages/` + path.split('packages/')[1] : path
|
||||
if (!cleaned.startsWith('packages/')) {
|
||||
throw new Error(`[packageChangedFromMainBranch] path=${cleaned} is not a valid package path`)
|
||||
}
|
||||
console.info(`[packageChangedFromMainBranch] path=${cleaned}`)
|
||||
|
||||
try {
|
||||
const diff = await exec(`git diff --quiet origin/main -- ${cleaned}`)
|
||||
return false
|
||||
}
|
||||
catch (e) {
|
||||
if ((e as ExecException).code === 1) {
|
||||
return true
|
||||
}
|
||||
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the package before publishing.
|
||||
* returns false if package can be published.
|
||||
* returns true if package is already published.
|
||||
* throws if validation fails.
|
||||
* @param path path of package to run pre-publishing checks for
|
||||
*/
|
||||
export const packagePrePublishChecks = async (path: string): Promise<boolean> => {
|
||||
console.info(`[packagePrePublishValidation] path=${path}`)
|
||||
|
||||
assert(path, '[packagePrePublishValidation] parameter "path" is required')
|
||||
|
||||
const { name: packageName, version: currentVersion } = await readPackageJson(path)
|
||||
const latestPublishedVersion = await getLatestPublishedVersion(packageName)
|
||||
const currentVersionAlreadyPublished = latestPublishedVersion !== null && currentVersion === latestPublishedVersion
|
||||
|
||||
if (currentVersionAlreadyPublished) {
|
||||
const packageChanged = await packageChangedFromMainBranch(path)
|
||||
|
||||
if (packageChanged) {
|
||||
throw new Error(`[packagePrePublishValidation] package version not incremented, path=${path}, version=${currentVersion}`)
|
||||
}
|
||||
|
||||
console.info(`[packagePrePublishValidation] package already published, path=${path}, version=${currentVersion}`)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
197
activepieces-fork/tools/scripts/utils/piece-script-utils.ts
Normal file
197
activepieces-fork/tools/scripts/utils/piece-script-utils.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
|
||||
import { readdir, stat } from 'node:fs/promises'
|
||||
import { resolve, join } from 'node:path'
|
||||
import { cwd } from 'node:process'
|
||||
import { extractPieceFromModule } from '@activepieces/shared'
|
||||
import * as semver from 'semver'
|
||||
import { readPackageJson } from './files'
|
||||
import { StatusCodes } from 'http-status-codes'
|
||||
import { execSync } from 'child_process'
|
||||
import { pieceTranslation,PieceMetadata } from '@activepieces/pieces-framework'
|
||||
type SubPiece = {
|
||||
name: string;
|
||||
displayName: string;
|
||||
version: string;
|
||||
minimumSupportedRelease?: string;
|
||||
maximumSupportedRelease?: string;
|
||||
metadata(): Omit<PieceMetadata, 'name' | 'version'>;
|
||||
};
|
||||
|
||||
export const AP_CLOUD_API_BASE = 'https://cloud.activepieces.com/api/v1';
|
||||
export const PIECES_FOLDER = 'packages/pieces'
|
||||
export const COMMUNITY_PIECE_FOLDER = 'packages/pieces/community'
|
||||
export const NON_PIECES_PACKAGES = ['@activepieces/pieces-framework', '@activepieces/pieces-common']
|
||||
|
||||
const validateSupportedRelease = (minRelease: string | undefined, maxRelease: string | undefined) => {
|
||||
if (minRelease !== undefined && !semver.valid(minRelease)) {
|
||||
throw Error(`[validateSupportedRelease] "minimumSupportedRelease" should be a valid semver version`)
|
||||
}
|
||||
|
||||
if (maxRelease !== undefined && !semver.valid(maxRelease)) {
|
||||
throw Error(`[validateSupportedRelease] "maximumSupportedRelease" should be a valid semver version`)
|
||||
}
|
||||
|
||||
if (minRelease !== undefined && maxRelease !== undefined && semver.gt(minRelease, maxRelease)) {
|
||||
throw Error(`[validateSupportedRelease] "minimumSupportedRelease" should be less than "maximumSupportedRelease"`)
|
||||
}
|
||||
}
|
||||
|
||||
const validateMetadata = (pieceMetadata: PieceMetadata): void => {
|
||||
console.info(`[validateMetadata] pieceName=${pieceMetadata.name}`)
|
||||
validateSupportedRelease(
|
||||
pieceMetadata.minimumSupportedRelease,
|
||||
pieceMetadata.maximumSupportedRelease,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const byDisplayNameIgnoreCase = (a: PieceMetadata, b: PieceMetadata) => {
|
||||
const aName = a.displayName.toUpperCase();
|
||||
const bName = b.displayName.toUpperCase();
|
||||
return aName.localeCompare(bName, 'en');
|
||||
};
|
||||
|
||||
export function getCommunityPieceFolder(pieceName: string): string {
|
||||
return join(COMMUNITY_PIECE_FOLDER, pieceName)
|
||||
}
|
||||
|
||||
|
||||
export async function findAllPiecesDirectoryInSource(): Promise<string[]> {
|
||||
const piecesPath = resolve(cwd(), 'packages', 'pieces')
|
||||
const paths = await traverseFolder(piecesPath)
|
||||
return paths
|
||||
}
|
||||
|
||||
export const pieceMetadataExists = async (
|
||||
pieceName: string,
|
||||
pieceVersion: string
|
||||
): Promise<boolean> => {
|
||||
const cloudResponse = await fetch(
|
||||
`${AP_CLOUD_API_BASE}/pieces/${pieceName}?version=${pieceVersion}`
|
||||
);
|
||||
|
||||
const pieceExist: Record<number, boolean> = {
|
||||
[StatusCodes.OK]: true,
|
||||
[StatusCodes.NOT_FOUND]: false
|
||||
};
|
||||
|
||||
if (
|
||||
pieceExist[cloudResponse.status] === null ||
|
||||
pieceExist[cloudResponse.status] === undefined
|
||||
) {
|
||||
throw new Error(await cloudResponse.text());
|
||||
}
|
||||
|
||||
return pieceExist[cloudResponse.status];
|
||||
};
|
||||
|
||||
export async function findNewPieces(): Promise<PieceMetadata[]> {
|
||||
const paths = await findAllDistPaths()
|
||||
const changedPieces: PieceMetadata[] = []
|
||||
|
||||
// Adding batches because of memory limit when we have a lot of pieces
|
||||
const batchSize = 75
|
||||
for (let i = 0; i < paths.length; i += batchSize) {
|
||||
const batch = paths.slice(i, i + batchSize)
|
||||
const batchResults = await Promise.all(batch.map(async (folderPath) => {
|
||||
const packageJson = await readPackageJson(folderPath);
|
||||
if (NON_PIECES_PACKAGES.includes(packageJson.name)) {
|
||||
return null;
|
||||
}
|
||||
const exists = await pieceMetadataExists(packageJson.name, packageJson.version)
|
||||
if (!exists) {
|
||||
try {
|
||||
return loadPieceFromFolder(folderPath);
|
||||
} catch (ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}))
|
||||
|
||||
const validResults = batchResults.filter((piece): piece is PieceMetadata => piece !== null)
|
||||
changedPieces.push(...validResults)
|
||||
}
|
||||
|
||||
return changedPieces;
|
||||
}
|
||||
|
||||
export async function findAllPieces(): Promise<PieceMetadata[]> {
|
||||
const paths = await findAllDistPaths()
|
||||
const pieces = await Promise.all(paths.map((p) => loadPieceFromFolder(p)))
|
||||
return pieces.filter((p): p is PieceMetadata => p !== null).sort(byDisplayNameIgnoreCase)
|
||||
}
|
||||
|
||||
async function findAllDistPaths(): Promise<string[]> {
|
||||
const baseDir = resolve(cwd(), 'dist', 'packages')
|
||||
const piecesBuildOutputPath = resolve(baseDir, 'pieces')
|
||||
return await traverseFolder(piecesBuildOutputPath)
|
||||
}
|
||||
|
||||
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 = 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
|
||||
}
|
||||
|
||||
async function loadPieceFromFolder(folderPath: string): Promise<PieceMetadata | null> {
|
||||
try {
|
||||
const packageJson = await readPackageJson(folderPath);
|
||||
|
||||
const packageLockPath = join(folderPath, 'package.json');
|
||||
const packageExists = await stat(packageLockPath).catch(() => null);
|
||||
if (packageExists) {
|
||||
console.info(`[loadPieceFromFolder] package.json exists, running bun install`)
|
||||
execSync('bun install', { cwd: folderPath, stdio: 'inherit' });
|
||||
}
|
||||
|
||||
const module = await import(
|
||||
join(folderPath, 'src', 'index')
|
||||
)
|
||||
|
||||
const { name: pieceName, version: pieceVersion } = packageJson
|
||||
const piece = extractPieceFromModule<SubPiece>({
|
||||
module,
|
||||
pieceName,
|
||||
pieceVersion
|
||||
});
|
||||
const originalMetadata = piece.metadata()
|
||||
const i18n = await pieceTranslation.initializeI18n(folderPath)
|
||||
const metadata = {
|
||||
...originalMetadata,
|
||||
name: packageJson.name,
|
||||
version: packageJson.version,
|
||||
i18n
|
||||
};
|
||||
metadata.directoryPath = folderPath;
|
||||
metadata.name = packageJson.name;
|
||||
metadata.version = packageJson.version;
|
||||
metadata.minimumSupportedRelease = piece.minimumSupportedRelease ?? '0.0.0';
|
||||
metadata.maximumSupportedRelease =
|
||||
piece.maximumSupportedRelease ?? '99999.99999.9999';
|
||||
|
||||
|
||||
validateMetadata(metadata);
|
||||
return metadata;
|
||||
}
|
||||
catch (ex) {
|
||||
console.error(ex)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
43
activepieces-fork/tools/scripts/utils/publish-nx-project.ts
Normal file
43
activepieces-fork/tools/scripts/utils/publish-nx-project.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import assert from 'node:assert'
|
||||
import { argv } from 'node:process'
|
||||
import { exec } from './exec'
|
||||
import { readPackageJson, readProjectJson } from './files'
|
||||
import { packagePrePublishChecks } from './package-pre-publish-checks'
|
||||
|
||||
export const publishNxProject = async (path: string): Promise<void> => {
|
||||
console.info(`[publishNxProject] path=${path}`)
|
||||
assert(path, '[publishNxProject] parameter "path" is required')
|
||||
|
||||
const packageAlreadyPublished = await packagePrePublishChecks(path);
|
||||
|
||||
if (packageAlreadyPublished) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { version } = await readPackageJson(path)
|
||||
const { name: nxProjectName } = await readProjectJson(path)
|
||||
|
||||
const nxPublishProjectCommand = `
|
||||
node tools/scripts/publish.mjs \
|
||||
${nxProjectName} \
|
||||
${version} \
|
||||
latest
|
||||
`
|
||||
|
||||
await exec(nxPublishProjectCommand)
|
||||
|
||||
console.info(`[publishNxProject] success, path=${path}, version=${version}`)
|
||||
}
|
||||
|
||||
const main = async (): Promise<void> => {
|
||||
const path = argv[2]
|
||||
await publishNxProject(path)
|
||||
}
|
||||
|
||||
/*
|
||||
* module is entrypoint, not imported i.e. invoked directly
|
||||
* see https://nodejs.org/api/modules.html#modules_accessing_the_main_module
|
||||
*/
|
||||
if (require.main === module) {
|
||||
main()
|
||||
}
|
||||
Reference in New Issue
Block a user