#!/usr/bin/env node import chalk from 'chalk'; import { program, Option } from 'commander'; import packageJson from '../package.json'; import { create } from './actions/create'; import { clone } from './actions/clone'; import { pull } from './actions/pull'; import { push } from './actions/push'; import { render } from './actions/render'; import { test } from './actions/test'; import { invoke } from './actions/invoke'; import { diff } from './actions/diff'; import { logs } from './actions/logs'; import { logUsage } from './helpers/log-usage'; import { ProductModuleCodeRunStatus } from './domain/product-module-code-run'; import { generate } from './actions/generate'; import { publish } from './actions/publish'; import { aiTest } from './actions/ai-test'; import { CLIError, ExitCodes, NetworkError, PlatformError } from './errors/platform-error'; import { tsBuild } from './actions/ts-build'; import { checkPackageAndNodeVersion } from './helpers/version-check'; // Custom error handler because commander does not support promise rejections const actionErrorHandler = (error: Error) => { logUsage({ args: process.argv, error: error.message }).catch(() => {}); if (error instanceof PlatformError) { console.error(chalk.red.bold('\nAPI Error')); console.error(chalk.red(error.message)); } else if (error instanceof NetworkError) { console.error(chalk.red.bold('\nNetwork Error')); console.error(chalk.red(error.message)); } else if (error instanceof CLIError) { console.error(chalk.red.bold('\nCLI Error')); console.error(chalk.red(error.message)); } else { console.error(chalk.red(`\n${error.message}`)); } // Use the CLIError class in order to specify the exit code const exitCode = error instanceof CLIError ? error.exitCode : ExitCodes.GENERAL_ERROR; process.exit(exitCode); }; const actionRunner = (fn: (...args: any) => Promise) => { return async (...args: any) => { try { await checkPackageAndNodeVersion(); await fn(...args); logUsage({ args: process.argv }).catch(() => {}); } catch (error) { actionErrorHandler(error as Error); } }; }; program.version(packageJson.version).description('Root platform command-line interface'); // This generates the schema files for all hooks. NB: This will later be extended to generate docs & payloads program .command('generate') .option( '-w, --workflow ', "generate the workflow schema for the specified step ('quote', 'application' or 'alterations')", ) .option( '-p, --payload ', "generate the payload for the specified step ('quote', 'application' or 'alterations')", ) .option( '--api-docs', "generate the Open API docs prompt ('./docs/api-docs-prompt.txt') used to produce the swagger spec at './docs/api-docs.json'", ) .description( "generates preliminary workflow schemas from \nJoi validation in the product module code, saving them to the './sandbox' directory.\nAdditionally, it creates payloads and writes them to the '/payloads' directory", ) .action(actionRunner(generate)); program .command('create ') .description('create a new product module on Root and pull the code base from Root into a newly created directory') .option('-h, --host ', 'specify an alternative host to interface with') .action( actionRunner((apiKey, productModuleName, productModuleKey, options) => create({ apiKey, productModuleName, productModuleKey, options }), ), ); program .command('clone ') .option('-l, --live', 'clone the live version') .option('-h, --host ', 'specify an alternative host to interface with') .option('--collection-module', 'specify whether to clone a collection module (default is to clone a product module)') .description('clone a product module from Root into a newly created directory') .action(actionRunner((apiKey, moduleKey, options) => clone({ moduleKey, options, apiKey }))); // This pulls the latest product module from Root and overrides everything locally to match. It's like a force pull from Github. program .command('pull') .option('-l, --live', 'pull the live version') .option('-f, --force', 'force the pull, ignoring the diff check') .option('--no-sort', 'do not sort object keys to match the local product module definition') .description( 'pull a product module from Root into the current product module directory. local changes will be overwritten', ) .action(actionRunner(pull)); // This pushes the changes to Root and becomes the latest draft overriding everything to match. It's like a force push to Github. program .command('push') .option('-f, --force', 'force the push, ignoring the diff check') .option('-t, --target ', 'push your current changes to a target product module specified by key') .option('--no-sort', 'do not sort object keys to match the local product module definition') .description( 'push a product module to Root from the current product module directory. changes on Root will be overwritten', ) .action(actionRunner(push)); // This publishes the most recent draft version of a product module // Currently not listed under help program .command('publish') .option('-f, --force', 'force the publish, ignoring the warning prompt') .description('publish the latest draft version of the product module') .action(actionRunner(publish)); // This allows the user to render html documents locally for testing purposes program .command('render') .option('-m, --merge', 'merge the merge-vars into the document files') .option('-w, --watch', 'watch for changes in templates or merge-vars') .option( '-p, --policy-id ', 'specify an existing sandbox policy id to be used as the merge vars. will be ignored if merge is set to false', ) .description('renders the document templates, optionally using the available merge vars') .action(actionRunner(render)); // This allows the user to run tests on the code locally program .command('test') .option('-u, --unit', 'run the unit test suite') .option('-w, --watch', 'watch for changes in code or test files') .option('-t, --template', 'create an template test file to run') // .option('-i, --integration', 'run the integration test suite') .description('runs the configured testing code') .action(actionRunner(test)); // Drive the module's quote/application/issue flows via a Claude+Playwright agent. // The dev authors scenarios in /test-plan.csv; this command runs each // scenario against sandbox, fetching credentials from 1Password. Companion to // `rp test` (unit tests on code/) — same module, two layers of coverage. program // hidden: keep ai-test runnable but off the `rp --help` command list, so it // stays an internal-only soft launch until we're ready to document it publicly. .command('ai-test', { hidden: true }) .option('--op-item ', '1Password item name holding the dashboard login (required)') .option('--op-vault ', 'optional 1Password vault to disambiguate the item title') .option('--dashboard-url ', 'override the dashboard URL (otherwise derived from .root-config.json host)') .option('--output-dir ', 'per-run output dir for mcp.json + screenshots / videos') .option('--step-timeout ', 'per-step replay budget before a step heals via the AI agent (default 15)') .option('--bail', 'stop after the first failing scenario') .option('--no-cache', 'disable record-and-replay; every scenario re-discovers its path from scratch') .option( '--video', 'record a replay.webm for every scenario (records then replays any uncached scenario); without this, runs are screenshot-only', ) .option( '--scenario-timeout ', 'max seconds the AI agent may spend recording/healing a single scenario before it is killed (default 600)', ) .description('drive test-plan.csv scenarios against sandbox via a Claude + Playwright agent') .action( actionRunner(async (options) => { // commander maps `--no-cache` to `options.cache === false`. const { exitCode } = await aiTest({ ...options, noCache: options.cache === false }); if (exitCode !== 0) process.exitCode = exitCode; }), ); // This allows the user to issue a policy from the CLI program .command('invoke') .option('-q, --quote', 'issue a quote') .option('-a, --application', 'issue an application') .option('-c, --claim', 'open a claim') .option('-l, --live', 'issue a policy from the live version') .option('-v --verbose', 'see the response payloads') .description('invoke the Root API (issues a policy by default)') .action(actionRunner(invoke)); // This allows the user to return execution logs for the hook invocations program .command('logs') .option('-p, --policy-id ', 'specify a policy ID to filter code runs by') .option('-f, --function-name ', 'specify a function name to filter code runs by') .addOption( new Option('-s, --status ', 'specify a status to filter code runs by').choices( Object.values(ProductModuleCodeRunStatus), ), ) .option('-c, --count ', 'specify the number of code runs to fetch logs for') .option( '-t, --time ', 'filter logs from the last duration (e.g. 30m, 1h, 2d) - collection module logs only', ) .addOption( new Option('-l, --level ', 'filter by log level - collection module logs only').choices([ 'info', 'warn', 'error', 'debug', ]), ) .option('-w, --watch', 'watch for new logs in real-time - collection module logs only') .description('display recent sandbox product module execution logs') .action(actionRunner(logs)); // This allows the user to run rp-diff to compare the local pm files and the remote pm files program .command('diff') .option('-c, --code', 'display differences for JavaScript files in ./code') .option('--unit-tests', 'display differences for JavaScript files in ./code/unit-tests') .option('--read-me', 'display differences in the readMe file') .option('-d, --documents', 'run the diff for document files only') .option('-w, --workflows', 'run the diff for workflow files only') .option('--api-docs', 'display the differences in the API docs only') .description( 'displays line-by-line differences between the local product module definition and the remote product module definition', ) .action(actionRunner(diff)); program .command('ts-build') .description('compile TypeScript source files from code/src/ into code/.build/') .action(actionRunner(tsBuild)); // eslint-disable-next-line @typescript-eslint/no-floating-promises (async () => { await program.parseAsync(); })();