Skip to main content
Glama
push.ts9.75 kB
import * as fsPromises from 'node:fs/promises'; import { join } from 'node:path'; import * as readline from 'node:readline'; import { getIntlayerAPIProxy } from '@intlayer/api'; import { formatPath, type ListGitFilesOptions, listGitFiles, parallelize, prepareIntlayer, writeContentDeclaration, } from '@intlayer/chokidar'; import { ANSIColors, type GetConfigurationOptions, getAppLogger, getConfiguration, } from '@intlayer/config'; import type { Dictionary } from '@intlayer/types'; import { getUnmergedDictionaries } from '@intlayer/unmerged-dictionaries-entry'; import { PushLogger, type PushStatus } from '../pushLog'; import { checkCMSAuth } from '../utils/checkAccess'; type PushOptions = { deleteLocaleDictionary?: boolean; keepLocaleDictionary?: boolean; dictionaries?: string[]; gitOptions?: ListGitFilesOptions; configOptions?: GetConfigurationOptions; build?: boolean; }; type DictionariesStatus = { dictionary: Dictionary; status: 'pending' | 'pushing' | 'modified' | 'pushed' | 'unknown' | 'error'; error?: Error; errorMessage?: string; }; // Print per-dictionary summary similar to loadDictionaries const statusIconsAndColors = { pushed: { icon: '✔', color: ANSIColors.GREEN }, modified: { icon: '✔', color: ANSIColors.GREEN }, error: { icon: '✖', color: ANSIColors.RED }, default: { icon: '⏲', color: ANSIColors.BLUE }, }; const getIconAndColor = (status: DictionariesStatus['status']) => { return ( statusIconsAndColors[status as keyof typeof statusIconsAndColors] ?? statusIconsAndColors.default ); }; /** * Get all local dictionaries and push them simultaneously. */ export const push = async (options?: PushOptions): Promise<void> => { const config = getConfiguration(options?.configOptions); const appLogger = getAppLogger(config, { config: { prefix: '', }, }); if (options?.build === true) { await prepareIntlayer(config, { forceRun: true }); } else if (typeof options?.build === 'undefined') { await prepareIntlayer(config); } try { const hasCMSAuth = await checkCMSAuth(config); if (!hasCMSAuth) return; const intlayerAPI = getIntlayerAPIProxy(undefined, config); const unmergedDictionariesRecord = getUnmergedDictionaries(config); let dictionaries: Dictionary[] = Object.values( unmergedDictionariesRecord ).flat(); const existingDictionariesKeys: string[] = Object.keys( unmergedDictionariesRecord ); if (options?.dictionaries) { // Check if the provided dictionaries exist const noneExistingDictionariesOption = options.dictionaries.filter( (dictionaryId) => !existingDictionariesKeys.includes(dictionaryId) ); if (noneExistingDictionariesOption.length > 0) { appLogger( `The following dictionaries do not exist: ${noneExistingDictionariesOption.join( ', ' )} and have been ignored.`, { level: 'error', } ); } // Filter the dictionaries from the provided list of IDs dictionaries = dictionaries.filter((dictionary) => options.dictionaries?.includes(dictionary.key) ); } if (options?.gitOptions) { const gitFiles = await listGitFiles(options.gitOptions); dictionaries = dictionaries.filter((dictionary) => gitFiles.includes( join(config.content.baseDir, dictionary.filePath ?? '') ) ); } // Check if the dictionaries list is empty if (dictionaries.length === 0) { appLogger('No local dictionaries found', { level: 'error', }); return; } appLogger('Pushing dictionaries:'); // Prepare dictionaries statuses const dictionariesStatuses: DictionariesStatus[] = dictionaries.map( (dictionary) => ({ dictionary, status: 'pending', }) ); // Initialize aggregated logger similar to loadDictionaries const logger = new PushLogger(); logger.update( dictionariesStatuses.map<PushStatus>((s) => ({ dictionaryKey: s.dictionary.key, status: 'pending', })) ); const successfullyPushedDictionaries: Dictionary[] = []; const processDictionary = async ( statusObj: DictionariesStatus ): Promise<void> => { statusObj.status = 'pushing'; logger.update([ { dictionaryKey: statusObj.dictionary.key, status: 'pushing' }, ]); try { const pushResult = await intlayerAPI.dictionary.pushDictionaries([ statusObj.dictionary, ]); const updatedDictionaries = pushResult.data?.updatedDictionaries ?? []; const newDictionaries = pushResult.data?.newDictionaries ?? []; const allDictionaries = [...updatedDictionaries, ...newDictionaries]; for (const remoteDictionaryData of allDictionaries) { const localDictionary = unmergedDictionariesRecord[ remoteDictionaryData.key ]?.find( (dictionary) => dictionary.localId === remoteDictionaryData.localId ); if (!localDictionary) continue; await writeContentDeclaration( { ...localDictionary, id: remoteDictionaryData.id }, config ); } if ( updatedDictionaries.some( (dictionary) => dictionary.key === statusObj.dictionary.key ) ) { statusObj.status = 'modified'; successfullyPushedDictionaries.push(statusObj.dictionary); logger.update([ { dictionaryKey: statusObj.dictionary.key, status: 'modified' }, ]); } else if ( newDictionaries.some( (dictionary) => dictionary.key === statusObj.dictionary.key ) ) { statusObj.status = 'pushed'; successfullyPushedDictionaries.push(statusObj.dictionary); logger.update([ { dictionaryKey: statusObj.dictionary.key, status: 'pushed' }, ]); } else { statusObj.status = 'unknown'; } } catch (error) { statusObj.status = 'error'; statusObj.error = error as Error; statusObj.errorMessage = `Error pushing dictionary ${statusObj.dictionary.key}: ${error}`; logger.update([ { dictionaryKey: statusObj.dictionary.key, status: 'error' }, ]); } }; // Process dictionaries in parallel with a concurrency limit (reuse parallelize) await parallelize(dictionariesStatuses, processDictionary, 5); // Stop the logger and render final state logger.finish(); for (const dictionaryStatus of dictionariesStatuses) { const { icon, color } = getIconAndColor(dictionaryStatus.status); appLogger( ` - ${dictionaryStatus.dictionary.key} ${ANSIColors.GREY}[${color}${icon} ${dictionaryStatus.status}${ANSIColors.GREY}]${ANSIColors.RESET}` ); } // Output any error messages for (const statusObj of dictionariesStatuses) { if (statusObj.errorMessage) { appLogger(statusObj.errorMessage, { level: 'error', }); } } // Handle delete or keep options const deleteOption = options?.deleteLocaleDictionary; const keepOption = options?.keepLocaleDictionary; if (deleteOption && keepOption) { throw new Error( 'Cannot specify both --deleteLocaleDictionary and --keepLocaleDictionary options.' ); } if (deleteOption) { // Delete only the successfully pushed dictionaries await deleteLocalDictionaries(successfullyPushedDictionaries, options); } else if (keepOption) { // Do nothing, keep the local dictionaries } else { // Ask the user const answer = await askUser( 'Do you want to delete the local dictionaries that were successfully pushed? (yes/no): ' ); if (answer.toLowerCase() === 'yes' || answer.toLowerCase() === 'y') { await deleteLocalDictionaries(successfullyPushedDictionaries, options); } } } catch (error) { appLogger(error, { level: 'error', }); } }; const askUser = (question: string): Promise<string> => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); return new Promise((resolve) => { rl.question(question, (answer: string) => { rl.close(); resolve(answer); }); }); }; const deleteLocalDictionaries = async ( dictionariesToDelete: Dictionary[], options?: PushOptions ): Promise<void> => { const config = getConfiguration(options?.configOptions); const appLogger = getAppLogger(config, { config: { prefix: '', }, }); // Use a Set to collect all unique file paths const filePathsSet: Set<string> = new Set(); for (const dictionary of dictionariesToDelete) { const { filePath } = dictionary; if (!filePath) { appLogger(`Dictionary ${dictionary.key} does not have a file path`, { level: 'error', }); continue; } filePathsSet.add(filePath); } for (const filePath of filePathsSet) { try { const stats = await fsPromises.lstat(filePath); if (stats.isFile()) { await fsPromises.unlink(filePath); appLogger(`Deleted file ${formatPath(filePath)}`, {}); } else if (stats.isDirectory()) { appLogger(`Path is a directory ${formatPath(filePath)}, skipping.`, {}); } else { appLogger( `Unknown file type for ${formatPath(filePath)}, skipping.`, {} ); } } catch (err) { appLogger(`Error deleting ${formatPath(filePath)}: ${err}`, { level: 'error', }); } } };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aymericzip/intlayer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server