import chalk from 'chalk'; import path from 'node:path'; import { checkValidProductModuleDirectory } from '../helpers/check-valid-product-module-directory'; import { checkDiff, displayDiffWarning } from '../helpers/high-level-diff'; import { fetchProductModuleDefinition } from '../helpers/fetch-product-module-definition'; import { readProductModuleDefinition } from '../helpers/read-product-module-definition'; import { writeProductModuleDefinition } from '../helpers/write-product-module-definition'; import { readJsConfigFiles } from '../helpers/jsconfig-helpers'; import { CLIProductModuleDefinition, productModuleDefinitionFromNetwork } from '../domain/product-module-definition'; import { updateStaticFiles } from '../helpers/update-static-files'; import { fetchSkillFiles } from '../helpers/fetch-skill-files'; import { writeSkillFiles } from '../helpers/write-skill-files'; import { readAuthAndConfig } from '../helpers/read-auth-and-config'; import { CollectionModuleConfig, isCollectionModuleConfig, } from '../domain/collection-modules/collection-module-config'; import { fetchCollectionModuleDefinition } from '../helpers/collection-modules/fetch-collection-module-definition'; import { collectionModuleDefinitionFromNetwork } from '../domain/collection-modules/collection-module-definition'; import { writeCollectionModuleDefinition } from '../helpers/collection-modules/write-collection-module-definition'; import { RootConfigWrite } from '../domain/root-config'; import { toProductModuleStack } from '../helpers/types-helpers'; import { runWithSpinner } from '../helpers/spinner'; import { symbols } from '../helpers/symbols'; interface PullOptions { live?: boolean; force?: boolean; sort?: boolean; } export const pullCollectionModule = async (params: { apiKey: string; collectionModuleConfig: CollectionModuleConfig; pullLive: boolean; }) => { const { apiKey, collectionModuleConfig, pullLive } = params; const { host, collectionModuleKey } = collectionModuleConfig; const directory = path.join('./'); const collectionModuleDefinitionNetwork = await runWithSpinner('Fetching collection module from Root...', () => fetchCollectionModuleDefinition({ apiKey, collectionModuleKey, host, pullLive }), ); const collectionModuleDefinition = collectionModuleDefinitionFromNetwork(collectionModuleDefinitionNetwork); await runWithSpinner('Writing collection module to directory...', () => writeCollectionModuleDefinition({ collectionModuleDefinition, directory, host, apiKey, liveVersion: pullLive }), ); console.log('\n' + chalk.green(`Collection module definition successfully written to current directory.`)); }; export const pullProductModule = async (params: { options: PullOptions; apiKey: string; productModuleConfig: RootConfigWrite; }) => { const { apiKey, options, productModuleConfig } = params; const pullLive = !!options.live; // Defaults to the draft version const forcePull = !!options.force; // Defaults to not forcing the pull const sortToMatchLocal = !!options.sort; // Defaults to sorting object keys const productModuleDirectory = path.join('./'); let localDefinition: CLIProductModuleDefinition | undefined; if (!forcePull || sortToMatchLocal) { try { checkValidProductModuleDirectory({ productModuleConfig }); localDefinition = sortToMatchLocal ? await readProductModuleDefinition(productModuleDirectory) : undefined; } catch (error) { if (error instanceof Error) { error.message += '\nUse "rp pull -f --no-sort" to force pull the remote product module.'; } throw error; } } const { host, productModuleKey } = productModuleConfig; // Fetch the product module definition from Root const remoteDefinitionNetwork = await runWithSpinner('Fetching product module from Root...', () => fetchProductModuleDefinition({ apiKey, productModuleKey, pullLive, host }), ); const remoteDefinition = productModuleDefinitionFromNetwork(remoteDefinitionNetwork); const stack = toProductModuleStack(remoteDefinition.rootConfig?.stack); if (!forcePull && localDefinition) { const diff = checkDiff(localDefinition, remoteDefinition); if (!diff.hasChanged) { console.log(`${symbols.check}No changes to pull, local version is in sync with Root`); return; } const answer = await displayDiffWarning(diff.changes); if (!answer) { console.log('Aborting pull. When you are ready, run the "rp pull" command again.'); return; } } // Read the existing jsconfig.json files, before the directory is overwritten const currentJsConfigs = await readJsConfigFiles(productModuleDirectory); // Now instantiate the product module into the folder the same way clone works await runWithSpinner('Writing product module to directory...', () => writeProductModuleDefinition({ productModuleDefinition: remoteDefinition, directory: productModuleDirectory, host, targetOrderDefinition: localDefinition, }), ); // Update static files await runWithSpinner('Updating static files...', () => updateStaticFiles({ productModuleDirectory, apiKey, host, currentJsConfigs, stack, }), ); // Refresh AI context skill files (.cursor/rules/ + docs/overview.md + CLAUDE.md) const skillFiles = await runWithSpinner('Refreshing AI context files...', () => fetchSkillFiles({ apiKey, host })); if (skillFiles && skillFiles.skills && skillFiles.skills.length > 0) { await writeSkillFiles({ directory: productModuleDirectory, skillFiles, productModuleName: remoteDefinition.productModuleName, }); } console.log( '\n' + chalk.green( `Version ${String(remoteDefinition.version)} of product module '${ remoteDefinition.key }' successfully pulled into current directory.`, ), ); }; export const pull = async (options: PullOptions) => { const { apiKey, config } = readAuthAndConfig(); await (isCollectionModuleConfig(config) ? pullCollectionModule({ apiKey, collectionModuleConfig: config, pullLive: !!options.live }) : pullProductModule({ apiKey, productModuleConfig: config, options })); };