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,4 @@
import { exec as execCallback } from 'node:child_process'
import { promisify } from 'node:util'
export const exec = promisify(execCallback)

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

View File

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

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

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