import chalk from 'chalk'; import fs, { promises as fsp } from 'node:fs'; import path from 'node:path'; import { fetchMergeVars } from '../helpers/fetch-merge-vars'; import { fetchProductModuleDefinition } from '../helpers/fetch-product-module-definition'; import { writeProductModuleDefinition } from '../helpers/write-product-module-definition'; import { writeMergeVars } from '../helpers/write-merge-vars'; import { 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 { writeFile } from '../helpers/write-file'; import { TopLevelFileName } from '../domain/file-names'; 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 { existsAsync } from '../helpers/fs-helpers'; import { writeCollectionModuleStaticFiles } from '../helpers/collection-modules/static-file-helpers'; import { ROOT_API_DEFAULT_HOST } from '../helpers/constants'; import { toProductModuleStack } from '../helpers/types-helpers'; import { runWithSpinner } from '../helpers/spinner'; import { symbols } from '../helpers/symbols'; interface CloneParams { apiKey: string; moduleKey: string; options: { live?: boolean; host?: string; collectionModule?: boolean }; } export const cloneCollectionModule = async (params: CloneParams) => { const { apiKey, options, moduleKey } = params; const { live, host: passedHost } = options; const host = passedHost || 'https://api.rootplatform.com'; const remoteCmDefinitionNetwork = await runWithSpinner('Fetching collection module from Root...', () => fetchCollectionModuleDefinition({ apiKey, collectionModuleKey: moduleKey, pullLive: !!live, host, }), ); const remoteCmDefinition = collectionModuleDefinitionFromNetwork(remoteCmDefinitionNetwork); const { collectionModuleKey } = remoteCmDefinition; // Create a new directory with the module key as the name const collectionModuleDirectory = path.join('./', collectionModuleKey); if (await existsAsync(collectionModuleDirectory)) { throw new Error( `Cannot create duplicate directory ${collectionModuleDirectory.toString()}. Please check if it is Root compatible and use \`rp pull\` inside the directory to fetch the product module, or delete the directory and try again.`, ); } await fsp.mkdir(collectionModuleDirectory); // Create an .root-auth file with the API key stored const rootAuthContent = `ROOT_API_KEY=${apiKey}`; const writeRootAuth = writeFile(path.join(collectionModuleDirectory, TopLevelFileName.RootAuth), rootAuthContent); const writeDefinition = writeCollectionModuleDefinition({ collectionModuleDefinition: remoteCmDefinition, directory: collectionModuleDirectory, host, apiKey, liveVersion: !!live, }); const writeStaticFiles = writeCollectionModuleStaticFiles({ directory: collectionModuleDirectory }); await runWithSpinner('Writing collection module to directory...', () => Promise.all([writeRootAuth, writeDefinition, writeStaticFiles]), ); console.log( '\n' + chalk.green(`Collection module '${collectionModuleKey}' successfully cloned into './${collectionModuleKey}'.`), ); }; export const cloneProductModule = async (params: CloneParams) => { const { apiKey, options, moduleKey } = params; const { live, host: passedHost } = options; const pullLive = !!live; // Defaults to the draft version const host: string = passedHost || ROOT_API_DEFAULT_HOST; // Fetch the product module from Root const remoteDefinitionNetwork = await runWithSpinner('Fetching product module from Root...', () => fetchProductModuleDefinition({ apiKey, productModuleKey: moduleKey, pullLive, host, }), ); const remoteDefinition = productModuleDefinitionFromNetwork(remoteDefinitionNetwork); // Create a new directory with the product module key as the name const productModuleDirectory = path.join('./', remoteDefinition.key); if (fs.existsSync(productModuleDirectory)) { throw new Error( `Cannot create duplicate directory ${productModuleDirectory.toString()}. Please check if it is Root compatible and use \`rp pull\` inside the directory to fetch the product module, or delete the directory and try again.`, ); } else { fs.mkdirSync(productModuleDirectory); } // Create a .gitignore file (ignoring the auth file) const gitignoreContent = '# Ignore Root authentication file\n.root-auth\nsandbox/output/*\n' + '# rp ai-test artifacts (cache + logged-in screenshots/videos) — never commit\nai-test/'; await writeFile(path.join(productModuleDirectory, '.gitignore'), gitignoreContent); // Create an .root-auth file with the API key stored const rootAuthContent = `ROOT_API_KEY=${apiKey}`; await writeFile(path.join(productModuleDirectory, TopLevelFileName.RootAuth), rootAuthContent); // Now instantiate the product module into the folder the same way pull works await runWithSpinner('Writing product module to directory...', () => writeProductModuleDefinition({ productModuleDefinition: remoteDefinition, directory: productModuleDirectory, host, }), ); const stack = toProductModuleStack(remoteDefinition.rootConfig?.stack); // Get and write merge vars (silent — matches `rp push` behavior for this step) const mergeVars = await fetchMergeVars({ apiKey, host }); await writeMergeVars({ directory: productModuleDirectory, mergeVars }); // Get and write static files await runWithSpinner('Updating static files...', () => updateStaticFiles({ productModuleDirectory, apiKey, host, stack, }), ); // Fetch and write 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( symbols.success + chalk.green(`Refreshed AI context files (.cursor/rules/, docs/overview.md, CLAUDE.md).`), ); } console.log( '\n' + chalk.green( `Version ${String(remoteDefinition.version)} of product module '${ remoteDefinition.key }' successfully cloned into './${remoteDefinition.key}'.`, ), ); }; export const clone = async (params: CloneParams) => { await (params.options.collectionModule ? cloneCollectionModule(params) : cloneProductModule(params)); };