Files
composiohq--composio/ts/packages/cli/scripts/_shared.ts
T
2026-07-13 12:38:34 +08:00

325 lines
9.8 KiB
TypeScript

import { builtinModules } from 'node:module';
import { chmod, copyFile, mkdir, readdir, rm, stat, writeFile } from 'node:fs/promises';
import * as path from 'node:path';
import process from 'node:process';
import { Cause, Effect, Exit } from 'effect';
import type { Teardown } from '@effect/platform/Runtime';
import { RUN_COMPANION_MODULE_BASENAMES } from '../src/services/run-companion-modules';
import { materializeAcpAdaptersCache } from './_acp-adapters';
const RUN_COMPANION_SERVICE_ENTRY_MAP = Object.fromEntries(
RUN_COMPANION_MODULE_BASENAMES.map(name => [`services/${name}`, `src/services/${name}.ts`])
) satisfies Record<string, string>;
const allowedRuntimeSpecifiers = new Set(
builtinModules.flatMap(specifier =>
specifier.startsWith('node:')
? [specifier, specifier.slice('node:'.length)]
: [specifier, `node:${specifier}`]
)
);
const importStatementPattern = /(?:^|[;\n])\s*import\s+(?:[^'"`\n]+?\s+from\s+)?["']([^"']+)["']/gm;
const exportStatementPattern =
/(?:^|[;\n])\s*export\s+(?:\*\s+from\s+|\{[^}\n]+\}\s+from\s+)["']([^"']+)["']/gm;
const runtimeImportCallPattern = /(?:^|[^\w$.])(?:__require|require|import)\(\s*["']([^"']+)["']/g;
const copyDirectoryRecursive = async (sourceDir: string, targetDir: string): Promise<void> => {
await mkdir(targetDir, { recursive: true });
for (const entry of await readdir(sourceDir, { withFileTypes: true })) {
const sourcePath = path.join(sourceDir, entry.name);
const targetPath = path.join(targetDir, entry.name);
if (entry.isDirectory()) {
await copyDirectoryRecursive(sourcePath, targetPath);
continue;
}
await copyFile(sourcePath, targetPath);
const mode = (await stat(sourcePath)).mode & 0o777;
await chmod(targetPath, mode || 0o755);
}
};
const copyBundledAcpAdapters = async (outputDir: string): Promise<void> => {
const acpAdaptersCacheDir = await materializeAcpAdaptersCache();
const acpOutputDir = path.join(outputDir, 'acp-adapters');
await rm(acpOutputDir, { force: true, recursive: true });
await copyDirectoryRecursive(acpAdaptersCacheDir, acpOutputDir);
};
export const LOCAL_TOOLS_BINARY_ASSET_DIRNAME = 'local-tools-binaries';
const localToolsBinaryAssetsSourceDir = (): string =>
path.resolve(process.cwd(), '../cli-local-tools', LOCAL_TOOLS_BINARY_ASSET_DIRNAME);
const copyLocalToolBinaryAssetsDirectory = async (outputDir: string): Promise<boolean> => {
const sourceDir = localToolsBinaryAssetsSourceDir();
const sourceExists = await stat(sourceDir)
.then(stats => stats.isDirectory())
.catch(() => false);
if (!sourceExists) return false;
const outputAssetDir = path.join(outputDir, LOCAL_TOOLS_BINARY_ASSET_DIRNAME);
await rm(outputAssetDir, { force: true, recursive: true });
await copyDirectoryRecursive(sourceDir, outputAssetDir);
return true;
};
export const copyLocalToolBinaryAssets = (outputDir: string) =>
Effect.gen(function* () {
const copied = yield* Effect.tryPromise(() => copyLocalToolBinaryAssetsDirectory(outputDir));
if (copied) {
yield* Effect.logDebug(
`Copied local tool binary assets into ${path.join(outputDir, LOCAL_TOOLS_BINARY_ASSET_DIRNAME)}`
);
}
});
const isAllowedRuntimeSpecifier = (specifier: string): boolean =>
specifier.startsWith('.') ||
specifier.startsWith('/') ||
specifier.startsWith('data:') ||
specifier === 'bun' ||
specifier.startsWith('bun:') ||
allowedRuntimeSpecifiers.has(specifier);
const stripStringsAndComments = (source: string): string => {
let result = '';
let index = 0;
const appendSpace = (char: string) => {
result += char === '\n' ? '\n' : ' ';
};
while (index < source.length) {
const char = source[index];
const next = source[index + 1];
if (char === "'" || char === '"' || char === '`') {
const quote = char;
appendSpace(char);
index += 1;
while (index < source.length) {
const current = source[index];
appendSpace(current);
index += 1;
if (current === '\\') {
if (index < source.length) {
appendSpace(source[index]!);
index += 1;
}
continue;
}
if (current === quote) {
break;
}
}
continue;
}
if (char === '/' && next === '/') {
appendSpace(char);
appendSpace(next);
index += 2;
while (index < source.length) {
const current = source[index];
appendSpace(current);
index += 1;
if (current === '\n') {
break;
}
}
continue;
}
if (char === '/' && next === '*') {
appendSpace(char);
appendSpace(next);
index += 2;
while (index < source.length) {
const current = source[index];
const following = source[index + 1];
appendSpace(current);
index += 1;
if (current === '*' && following === '/') {
appendSpace(following);
index += 1;
break;
}
}
continue;
}
result += char;
index += 1;
}
return result;
};
const collectBundledSpecifiers = (source: string): ReadonlyArray<string> => {
const sanitizedSource = stripStringsAndComments(source);
const specifiers = new Set<string>();
for (const pattern of [
importStatementPattern,
exportStatementPattern,
runtimeImportCallPattern,
]) {
for (const match of sanitizedSource.matchAll(pattern)) {
const specifier = match[1];
if (specifier) {
specifiers.add(specifier);
}
}
}
return [...specifiers];
};
const collectJavaScriptFiles = async (directory: string): Promise<ReadonlyArray<string>> => {
const collected: string[] = [];
const walk = async (directory: string): Promise<void> => {
for (const entry of await readdir(directory, { withFileTypes: true })) {
const entryPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
await walk(entryPath);
continue;
}
if (/\.(?:m?js|cjs)$/.test(entry.name)) {
collected.push(entryPath);
}
}
};
await walk(directory);
return collected.sort();
};
const collectRuntimeFiles = async (rootDir: string): Promise<ReadonlyArray<string>> => {
const collected = new Set<string>();
for (const baseName of RUN_COMPANION_MODULE_BASENAMES) {
const wrapperPath = path.join(rootDir, `${baseName}.mjs`);
if (await Bun.file(wrapperPath).exists()) {
collected.add(wrapperPath);
}
}
for (const relativeDir of ['services', 'acp-adapters']) {
const absoluteDir = path.join(rootDir, relativeDir);
const exists = await stat(absoluteDir)
.then(stats => stats.isDirectory())
.catch(() => false);
if (!exists) {
continue;
}
for (const filePath of await collectJavaScriptFiles(absoluteDir)) {
collected.add(filePath);
}
}
return [...collected].sort();
};
const assertBundledRuntimeFiles = async (rootDir: string): Promise<void> => {
const files = await collectRuntimeFiles(rootDir);
const violations: string[] = [];
for (const filePath of files) {
const source = await Bun.file(filePath).text();
for (const specifier of collectBundledSpecifiers(source)) {
if (!specifier || isAllowedRuntimeSpecifier(specifier)) {
continue;
}
violations.push(`${path.relative(rootDir, filePath)} -> ${specifier}`);
}
}
if (violations.length > 0) {
throw new Error(
[
'Generated runtime support files still reference external packages:',
...violations.map(violation => ` - ${violation}`),
].join('\n')
);
}
};
const buildCompanionServiceBundles = async (outputDir: string): Promise<void> => {
const servicesOutputDir = path.join(outputDir, 'services');
await rm(servicesOutputDir, { force: true, recursive: true });
await mkdir(servicesOutputDir, { recursive: true });
for (const [entryName, entryPath] of Object.entries(RUN_COMPANION_SERVICE_ENTRY_MAP)) {
const relativeOutputPath = `${entryName}.mjs`;
const outputDirectory = path.join(outputDir, path.dirname(relativeOutputPath));
const outputFileName = path.basename(relativeOutputPath);
await mkdir(outputDirectory, { recursive: true });
const result = await Bun.build({
entrypoints: [path.resolve(entryPath)],
outdir: outputDirectory,
naming: outputFileName,
target: 'bun',
format: 'esm',
packages: 'bundle',
sourcemap: 'none',
});
if (!result.success) {
const details = result.logs
.map(log => log.message)
.filter(message => message.length > 0)
.join('\n');
throw new Error(
details.length > 0
? `Failed to bundle ${entryName}:\n${details}`
: `Failed to bundle ${entryName}.`
);
}
}
};
/**
* Shared teardown for all CLI scripts.
*
* Exits with a non-zero code when the Effect program fails
* (unless the failure is an interrupt-only cause).
*/
export const teardown: Teardown = <E, A>(exit: Exit.Exit<E, A>, onExit: (code: number) => void) => {
const shouldFail = Exit.isFailure(exit) && !Cause.isInterruptedOnly(exit.cause);
const errorCode = Number(process.exitCode ?? 1);
onExit(shouldFail ? errorCode : 0);
};
export const buildCompanionModules = (outputDir: string) =>
Effect.gen(function* () {
yield* Effect.tryPromise(() => mkdir(outputDir, { recursive: true }));
yield* Effect.tryPromise(() => buildCompanionServiceBundles(outputDir));
for (const name of RUN_COMPANION_MODULE_BASENAMES) {
const wrapperPath = path.join(outputDir, `${name}.mjs`);
const wrapperSource = `export * from "./services/${name}.mjs";\n`;
yield* Effect.tryPromise(() => writeFile(wrapperPath, wrapperSource, 'utf8'));
}
yield* Effect.tryPromise(() => copyBundledAcpAdapters(outputDir));
yield* Effect.tryPromise(() => assertBundledRuntimeFiles(outputDir));
});