@underpostnet/underpost
Version:
Underpost Platform — end-to-end CI/CD and application-delivery toolchain CLI. Covers bare metal, Kubernetes, K3s, kubeadm, LXD, container/image orchestration, secrets, databases, cron jobs, monitoring, SSH, runners, PWA + Workbox delivery, and release orc
841 lines (799 loc) • 61.6 kB
JavaScript
import dotenv from 'dotenv';
import fs from 'fs-extra';
import { Command } from 'commander';
import { getNpmRootPath, getUnderpostRootPath, loadConf } from '../server/conf.js';
import { commitData } from '../client/components/core/CommonJs.js';
import Underpost from '../index.js';
const underpostGlobalEnv = `${getUnderpostRootPath()}/.env`;
if (fs.existsSync(underpostGlobalEnv)) dotenv.config({ path: underpostGlobalEnv, override: true, quiet: true });
else dotenv.config({ quiet: true });
const program = new Command();
program.name('underpost').description(`underpost ci/cd cli ${Underpost.version}`).version(Underpost.version);
program
.command('new')
.argument('[app-name]', 'The name of the new project.')
.option('--deploy-id <deploy-id>', 'Create deploy ID conf env files')
.option('--sub-conf <sub-conf>', 'Create sub conf env files')
.option('--cluster', 'Create deploy ID cluster files and sync to current cluster')
.option('--build-repos', 'Create deploy ID repositories')
.option('--build', 'Build the deployment to pwa-microservices-template (requires --deploy-id)')
.option('--clean-template', 'Clean the build directory (pwa-microservices-template)')
.option('--sync-conf', 'Sync configuration to private repositories (requires --deploy-id)')
.option(
'--sync-start',
"Sync start scripts in deploy ID package.json with root package.json (use 'dd' as --deploy-id to sync all dd.router)",
)
.option('--purge', 'Remove deploy ID conf and all related repositories (requires --deploy-id)')
.option('--dev', 'Sets the development cli context')
.option('--default-conf', 'Create default deploy ID conf env files')
.option('--conf-workflow-id <workflow-id>', 'Set custom configuration workflow ID for conf generation')
.description('Initializes a new Underpost project, service, or configuration.')
.action(Underpost.repo.new);
program
.command('client')
.argument('[deploy-id]', 'The deployment ID to build.', 'dd-default')
.argument('[sub-conf]', 'The sub-configuration for the build.', '')
.argument('[host]', 'Comma-separated hosts to filter the build.', '')
.argument('[path]', 'Comma-separated paths to filter the build.', '')
.option('--sync-env-port', 'Sync environment port assignments across all deploy IDs')
.option('--single-replica', 'Build single replica folders instead of full client')
.option('--build-zip', 'Create zip files of the builds')
.option('--split <mb>', 'Split generated zip files into parts of the specified size in MB')
.option('--unzip <build-prefix>', 'Extract a built client zip or split zip parts using the given build prefix')
.option('--merge-zip <build-prefix>', 'Merge split ZIP parts back into a single ZIP file for the given build prefix')
.option('--lite-build', 'Skip full build (default is full build)')
.option('--icons-build', 'Build icons')
.description('Builds client assets, single replicas, and/or syncs environment ports.')
.action(Underpost.repo.client);
program
.command('start')
.argument('<deploy-id>', 'The unique identifier for the deployment configuration.')
.argument(
'[env]',
'Optional: The environment to start (e.g., "development", "production"). Defaults to "development".',
)
.option('--run', 'Starts application servers and monitors their health.')
.option('--build', 'Triggers the client-side application build process.')
.option('--underpost-quickly-install', 'Uses Underpost Quickly Install for dependency installation.')
.option('--skip-pull-base', 'Skips cloning repositories, uses current workspace code directly.')
.option('--skip-full-build', 'Skips the full client bundle build during deployment.')
.option(
'--pull-bundle',
'Downloads the pre-built client bundle from Cloudinary via pull-bundle before starting. Use together with --skip-full-build to skip the local build entirely.',
)
.option(
'--private-test-repo',
'During --build, clone the private test source repo (engine-test-<id>) instead of the production engine-<id> repo.',
)
.action(Underpost.start.callback)
.description('Initiates application servers, build pipelines, or other defined services based on the deployment ID.');
program
.command('clone')
.argument(`<uri>`, 'The URI of the GitHub repository (e.g., "username/repository").')
.option('--bare', 'Performs a bare clone, downloading only the .git files.')
.option('--g8', 'Uses the g8 repository extension for cloning.')
.description('Clones a specified GitHub repository into the current directory.')
.action(Underpost.repo.clone);
program
.command('pull')
.argument('<path>', 'The absolute or relative directory path where the repository is located.')
.argument(`<uri>`, 'The URI of the GitHub repository (e.g., "username/repository").')
.description('Pulls the latest changes from a specified GitHub repository.')
.option('--g8', 'Uses the g8 repository extension for pulling.')
.action(Underpost.repo.pull);
program
.command('cmt')
.argument('[path]', 'The absolute or relative directory path of the repository.')
.argument(`[commit-type]`, `The type of commit to perform. Options: ${Object.keys(commitData).join(', ')}.`)
.argument(`[module-tag]`, 'Optional: Sets a specific module tag for the commit.')
.argument(`[message]`, 'Optional: Provides an additional custom message for the commit.')
.option(`--log [latest-n]`, 'Shows commit history from the specified number of latest n path commits.')
.option('--last-msg <latest-n>', 'Displays the last n commit message.')
.option('--empty', 'Allows committing with empty files.')
.option('--copy', 'Copies the generated commit message to the clipboard.')
.option('--info', 'Displays information about available commit types.')
.option('--diff', 'Shows the current git diff changes.')
.option('--edit', 'Edit last commit.')
.option('--deploy-id <deploy-id>', 'Sets the deployment configuration ID for the commit context.')
.option('--cached', 'Commit staged changes only or context.')
.option('--hashes <hashes>', 'Comma-separated list of specific file hashes of commits.')
.option('--extension <extension>', 'specific file extensions of commits.')
.option('--changelog', 'Print the plain changelog of the last N commits (see --from-n-commit, default 1).')
.option('--changelog-build', 'Builds a CHANGELOG.md file based on the commit history')
.option('--changelog-min-version <version>', 'Sets the minimum version limit for --changelog-build (default: 2.85.0)')
.option(
'--changelog-no-hash',
'Excludes commit hashes from the generated changelog entries (used with --changelog-build).',
)
.option(
'--changelog-msg',
'Print the sanitized, commit-ready changelog message of the last N commits (see --from-n-commit, default 1). Empty when there are no tagged entries.',
)
.option('--from-n-commit <n>', 'Number of latest commits to include in --changelog/--changelog-msg (default: 1).')
.option('--unpush', 'With --log, automatically sets range to unpushed commits ahead of remote.')
.option('-b', 'Shows the current Git branch name.')
.option('-p [branch]', 'Shows the reflog for the specified branch.')
.option('--bc <commit-hash>', 'Shows branches that contain the specified commit.')
.option(
'--is-remote-repo <url-repo>',
'Checks whether a remote Git repository URL is reachable. Prints true or false.',
)
.option(
'--has-changes',
'Prints "1" if there are staged or unstaged git changes in the repository, empty string otherwise.',
)
.option('--remote-url', 'Prints the current git remote URL (origin) in plain text.')
.option(
'--switch-repo <url>',
'Switches the git remote (origin) to <url> and force-pulls the target branch, overwriting the current working tree (discards local commits and tracked changes). Accepts a full URL or "owner/repo".',
)
.option('--target-branch <branch>', 'Target branch for --switch-repo (default: master).')
.description('Manages commits to a GitHub repository, supporting various commit types and options.')
.action(Underpost.repo.commit);
program
.command('push')
.argument('<path>', 'The absolute or relative directory path of the repository.')
.argument(`<uri>`, 'The URI of the GitHub repository (e.g., "username/repository").')
.option('-f', 'Forces the push, overwriting the remote repository history.')
.option('--g8', 'Uses the g8 repository extension for pushing.')
.description('Pushes committed changes from a local repository to a remote GitHub repository.')
.action(Underpost.repo.push);
program
.command('env')
.argument(
'[deploy-id]',
`The deployment configuration ID. Use 'clean' to restore default environment settings. Use 'root' to load underpost root env. Use 'current' to get plain current deploy Id.`,
)
.argument('[env]', 'Optional: The environment to set (e.g., "production", "development"). Defaults to "production".')
.argument('[subConf]', 'Optional: The sub configuration to set.')
.description('Sets environment variables and configurations related to a specific deployment ID.')
.action((deployId, env, subConf) => {
if (deployId === 'root') {
const underpostRootDeployId = Underpost.env.get('DEPLOY_ID');
if (underpostRootDeployId) deployId = underpostRootDeployId;
}
if (env) process.env.NODE_ENV = env;
loadConf(deployId, subConf);
});
program
.command('static')
.option('--page <ssr-component-path>', 'Build custom static pages.')
.option('--title <title>', 'Sets a custom title for the static page (deprecated: use --config-file).')
.option('--output-path <output-path>', 'Sets the output path for the generated static page.')
// Metadata options
.option('--description <description>', 'Page description for SEO.')
.option('--keywords <keywords>', 'Comma-separated keywords for SEO.')
.option('--author <author>', 'Page author.')
.option('--theme-color <color>', 'Theme color for mobile browsers.')
.option('--canonical-url <url>', 'Canonical URL for SEO.')
.option('--thumbnail <url>', 'Open Graph thumbnail image URL.')
.option('--locale <locale>', 'Page locale (default: en-US).')
.option('--site-name <name>', 'Site name for Open Graph.')
.option('--head-scripts <paths>', 'Comma-separated paths to scripts for head section.')
.option('--body-scripts <paths>', 'Comma-separated paths to scripts for body section.')
.option('--styles <paths>', 'Comma-separated paths to stylesheets.')
.option('--favicon <path>', 'Favicon path.')
.option('--apple-touch-icon <path>', 'Apple touch icon path.')
.option('--manifest <path>', 'Web manifest path.')
.option('--head-components <paths>', 'Comma-separated SSR head component paths.')
.option('--body-components <paths>', 'Comma-separated SSR body component paths.')
.option('--build-path <build-path>', 'Sets a custom build path for static documents or assets.')
.option('--env <env>', 'Sets the environment for the static build (e.g., "development", "production").')
.option('--minify', 'Minify HTML output (default: true for production).')
.option('--no-minify', 'Disable HTML minification.')
.option('--config-file <path>', 'Path to JSON configuration file.')
.option('--generate-config [path]', 'Generate a template configuration file.')
.option('--lang <lang>', 'HTML lang attribute (default: en).')
.option('--dir <dir>', 'HTML dir attribute (default: ltr).')
.option('--dev', 'Sets the development cli context')
.option(
'--run-sv [port]',
'Start a standalone Express static server to preview the static build (default port: 5000).',
)
.description(`Manages static build of page, bundles, and documentation with comprehensive customization options.`)
.action(Underpost.static.callback);
program
.command('config')
.argument('operator', `The configuration operation to perform. Options: ${Object.keys(Underpost.env).join(', ')}.`)
.argument('[key]', 'Optional: The specific configuration key to manage.')
.argument('[value]', 'Optional: The value to set for the configuration key.')
.option('--plain', 'Prints the configuration value in plain text.')
.option('--filter <keyword>', 'Filters the list by matching key or value (only for list operation).')
.option('--deploy-id <deploy-id>', 'Sets the deployment configuration ID for the operation context.')
.option('--build', 'Sets the build context for the operation.')
.option('--copy', 'Copies the configuration value to the clipboard (only for get operation).')
.description(`Manages Underpost configurations using various operators.`)
.action((...args) => Underpost.env[args[0]](args[1], args[2], args[3]));
program
.command('root')
.description('Displays the root path of the npm installation.')
.action(() => console.log(getNpmRootPath()));
program
.command('ip')
.argument('[ips]', 'Optional args comma-separated list of IP to process.')
.option('--dhcp', 'Fetches and displays the current Dynamic Host Configuration Protocol server IP address.')
.option('--copy', 'Copies the IP addresses to the clipboard.')
.option('--ban-ingress-add', 'Adds IP addresses to banned ingress list.')
.option('--ban-ingress-remove', 'Removes IP addresses from banned ingress list.')
.option('--ban-ingress-list', 'Lists all banned ingress IP addresses.')
.option('--ban-ingress-clear', 'Clears all banned ingress IP addresses.')
.option('--ban-egress-add', 'Adds IP addresses to banned egress list.')
.option('--ban-egress-remove', 'Removes IP addresses from banned egress list.')
.option('--ban-egress-list', 'Lists all banned egress IP addresses.')
.option('--ban-egress-clear', 'Clears all banned egress IP addresses.')
.option('--ban-both-add', 'Adds IP addresses to both banned ingress and egress lists.')
.option('--ban-both-remove', 'Removes IP addresses from both banned ingress and egress lists.')
.option('--mac', 'Prints the MAC address of the main network interface.')
.description('Displays the current public machine IP addresses.')
.action(Underpost.dns.ipDispatcher);
program
.command('cluster')
.argument('[pod-name]', 'Optional: Filters information by a specific pod name.')
.option('--reset', `Deletes all clusters and prunes all related data and caches.`)
.option(
'--reset-mongodb',
`Performs a hard cleanup of only MongoDB-related resources (StatefulSet, PVCs/PVs, Secrets, ConfigMaps, caches) without restarting the whole node.`,
)
.option('--mariadb', 'Initializes the cluster with a MariaDB statefulset.')
.option('--mysql', 'Initializes the cluster with a MySQL statefulset.')
.option('--mongodb', 'Initializes the cluster with a MongoDB statefulset.')
.option('--service-host <host>', 'Set custom host/IP for exposed MongoDB and Valkey clients.')
.option('--postgresql', 'Initializes the cluster with a PostgreSQL statefulset.')
.option('--mongodb4', 'Initializes the cluster with a MongoDB 4.4 service.')
.option('--valkey', 'Initializes the cluster with a Valkey service.')
.option('--ipfs', 'Initializes the cluster with an ipfs-cluster statefulset.')
.option('--contour', 'Initializes the cluster with Project Contour base HTTPProxy and Envoy.')
.option(
'--node-port',
'Exposes enabled ready services (e.g. MongoDB 4.4, Valkey) to the host/public network via their NodePort Service manifest.',
)
.option(
'--node-selector <k8s-node-name>',
'Pins the just-deployed StatefulSet (MongoDB 4.4 / Valkey) to the given Kubernetes node once it is ready (via a kubernetes.io/hostname nodeSelector).',
)
.option('--cert-manager', "Initializes the cluster with a Let's Encrypt production ClusterIssuer.")
.option('--dedicated-gpu', 'Initializes the cluster with dedicated GPU base resources and environment settings.')
.option(
'--ns-use <ns-name>',
"Switches the current Kubernetes context to the specified namespace (creates if it doesn't exist).",
)
.option('--kubeadm', 'Initializes the cluster using kubeadm for control plane management.')
.option(
'--pod-network-cidr <cidr>',
'Sets custom pod network CIDR for kubeadm cluster initialization (defaults to "192.168.0.0/16").',
)
.option(
'--control-plane-endpoint <endpoint>',
'Sets custom control plane endpoint for kubeadm cluster initialization (defaults to "localhost:6443").',
)
.option('--grafana', 'Initializes the cluster with a Grafana deployment.')
.option(
'--prom [hosts]',
'Initializes the cluster with a Prometheus Operator deployment and monitor scrap for specified hosts.',
)
.option('--dev', 'Initializes a development-specific cluster configuration.')
.option('--list-pods', 'Displays detailed information about all pods.')
.option('--pull-image', 'Sets an optional associated image to pull during initialization.')
.option('--init-host', 'Installs necessary Kubernetes node CLI tools (e.g., kind, kubeadm, docker, podman, helm).')
.option('--uninstall-host', 'Uninstalls all host components installed by init-host.')
.option('--config', 'Sets the base Kubernetes node configuration.')
.option('--chown', 'Sets the appropriate ownership for Kubernetes kubeconfig files.')
.option('--k3s', 'Initializes the cluster using K3s (Lightweight Kubernetes).')
.option('--hosts <hosts>', 'A comma-separated list of cluster hostnames or IP addresses.')
.option('--remove-volume-host-paths', 'Removes specified volume host paths after execution.')
.option(
'--reset-mode <mode>',
'Reset mode for --reset --k3s: "drain" (stop services, keep K3s installed) or "full" (uninstall + cleanup). Default: "full".',
)
.option('--namespace <namespace>', 'Kubernetes namespace for cluster operations (defaults to "default").')
.option('--replicas <replicas>', 'Sets a custom number of replicas for statefulset deployments.')
.action(Underpost.cluster.init)
.description('Manages Kubernetes clusters, defaulting to Kind cluster initialization.');
program
.command('deploy')
.argument('[deploy-list]', 'A comma-separated list of deployment IDs (e.g., "default-a,default-b").')
.argument(
'[env]',
'Optional: The environment for deployment (e.g., "development", "production"). Defaults to "development".',
)
.option('--remove', 'Deletes specified deployments and their associated services.')
.option('--sync', 'Synchronizes deployment environment variables, ports, and replica counts.')
.option('--info-router', 'Displays the current router structure and configuration.')
.option('--expose', 'Exposes services matching the provided deployment ID list.')
.option('--cert', 'Resets TLS/SSL certificate secrets for deployments.')
.option('--cert-hosts <hosts>', 'Resets TLS/SSL certificate secrets for specified hosts.')
.option(
'--self-signed',
'Use a pre-created self-signed TLS secret (kubernetes.io/tls) instead of cert-manager. ' +
'The secret must already exist in the namespace with the same name as the host. ' +
'Enables TLS in the Contour HTTPProxy virtualhost without requiring a production ClusterIssuer.',
)
.option('--node <node>', 'Sets optional node for deployment operations.')
.option(
'--ssh-key-path <path>',
'Private key path for node SSH operations. Currently used when shipping a hostPath volume to a remote target node over SSH. Defaults to engine-private/deploy/id_rsa.',
)
.option(
'--build-manifest',
'Builds Kubernetes YAML manifests, including deployments, services, proxies, and secrets.',
)
.option('--replicas <replicas>', 'Sets a custom number of replicas for deployments.')
.option('--image <image>', 'Sets a custom image for deployments.')
.option('--versions <deployment-versions>', 'A comma-separated list of custom deployment versions.')
.option('--traffic <traffic-versions>', 'A comma-separated list of custom deployment traffic weights.')
.option(
'--timeout-response <duration>',
'Sets HTTPProxy per-route response timeout (e.g., "1s", "300ms", "infinity").',
)
.option('--timeout-idle <duration>', 'Sets HTTPProxy per-route idle timeout (e.g., "10s", "infinity").')
.option('--retry-count <count>', 'Sets HTTPProxy per-route retry count (e.g., 3).')
.option('--retry-per-try-timeout <duration>', 'Sets HTTPProxy retry per-try timeout (e.g., "150ms").')
.option('--disable-update-deployment', 'Disables updates to deployments.')
.option('--disable-runtime-probes', 'Omits the internal-status HTTP probes from generated deployment manifests.')
.option('--tcp-probes', 'Generates legacy TCP socket probes instead of HTTP internal-status probes (migration).')
.option('--disable-update-proxy', 'Disables updates to proxies.')
.option('--disable-deployment-proxy', 'Disables proxies of deployments.')
.option('--disable-update-volume', 'Disables updates to volume mounts during deployment.')
.option(
'--status',
'Retrieves current network traffic data from resource deployments and the host machine network configuration.',
)
.option('--kubeadm', 'Enables the kubeadm context for deployment operations.')
.option('--k3s', 'Enables the k3s context for deployment operations.')
.option('--kind', 'Enables the kind context for deployment operations.')
.option('--git-clean', 'Runs git clean on volume mount paths before copying.')
.option('--disable-update-underpost-config', 'Disables updates to Underpost configuration during deployment.')
.option('--namespace <namespace>', 'Kubernetes namespace for deployment operations (defaults to "default").')
.option('--kind-type <kind-type>', 'Specifies the Kind cluster type for deployment operations.')
.option('--port <port>', 'Sets up port forwarding from local to remote ports.')
.option(
'--expose-port <port>',
'Sets the local:remote port to expose when --expose is active (overrides auto-detected service port).',
)
.option(
'--expose-local-port <port>',
'Sets a different local port for --expose (e.g. 80) while keeping the remote service port. Useful for /etc/hosts local access without specifying a port in the browser.',
)
.option(
'--local-proxy',
'Forward all service TCP ports locally and start the Node.js path-routing proxy. Enables full path-based routing (e.g. /wp alongside /) without needing --expose-local-port. Requires --expose.',
)
.option('--cmd <cmd>', 'Custom initialization command for deployment (comma-separated commands).')
.option(
'--skip-full-build',
'Skip client bundle rebuild; container will pull pre-built bundle via pull-bundle instead.',
)
.option(
'--pull-bundle',
'Explicitly pull the pre-built client bundle from Cloudinary inside the container. Use together with --skip-full-build.',
)
.option(
'--image-pull-policy <policy>',
'Override container imagePullPolicy in the generated deployment manifest (Always, IfNotPresent, Never). Defaults to Never for localhost/ images and IfNotPresent otherwise.',
)
.option(
'--tls',
'Enables TLS for the local proxy started by --expose --local-proxy. ' +
'The proxy will serve HTTPS on port 443 using self-signed certificates resolved from the local SSL store. ' +
'Use together with --expose and --local-proxy.',
)
.description('Manages application deployments, defaulting to deploying development pods.')
.action(Underpost.deploy.callback);
program
.command('secret')
.argument('<platform>', `The secret management platform. Options: ${Object.keys(Underpost.secret).join(', ')}.`)
.option('--init', 'Initializes the secrets platform environment.')
.option('--create-from-file <path-env-file>', 'Creates secrets from a specified environment file.')
.option('--create-from-env', 'Creates secrets from container environment variables (envFrom: secretRef).')
.option('--global-clean', 'Removes all filesystem traces of secrets (engine-private, .env, conf cache).')
.option('--list', 'Lists all available secrets for the platform.')
.description(`Manages secrets for various platforms.`)
.action((...args) => {
if (args[1].globalClean) return Underpost.secret.globalSecretClean();
if (args[1].createFromFile) return Underpost.secret[args[0]].createFromEnvFile(args[1].createFromFile);
if (args[1].createFromEnv) return Underpost.secret[args[0]].createFromContainerEnv();
if (args[1].list) return Underpost.secret[args[0]].list();
if (args[1].init) return Underpost.secret[args[0]].init();
});
program
.command('image')
.option(
'--build',
'Builds a Docker image using Podman, optionally saves it as a tar archive, and loads it into a specified Kubernetes cluster (Kind, Kubeadm, or K3s).',
)
.option('--ls', 'Lists all available Underpost Dockerfile images.')
.option('--rm <image-id>', 'Removes specified Underpost Dockerfile images.')
.option('--path [path]', 'The path to the Dockerfile directory.')
.option('--image-name [image-name]', 'Sets a custom name for the Docker image.')
.option('--image-out-path [image-out-path]', 'Sets the output path for the tar image archive.')
.option('--dockerfile-name [dockerfile-name]', 'Sets a custom name for the Dockerfile.')
.option('--podman-save', 'Exports the built image as a tar file using Podman.')
.option('--pull-base', 'Pulls the base image prerequisites (rockylinux:9) on the host; combine with --build.')
.option('--spec', 'Get current cached list of container images used by all pods')
.option('--namespace <namespace>', 'Kubernetes namespace for image operations (defaults to "default").')
.option('--kind', 'Set kind cluster env image context management.')
.option('--kubeadm', 'Set kubeadm cluster env image context management.')
.option('--k3s', 'Set k3s cluster env image context management.')
.option('--docker-compose', 'Load the built image tar into the local Docker store for Docker Compose availability.')
.option('--node-name', 'Set node name for kubeadm or k3s cluster env image context management.')
.option('--reset', 'Performs a build without using the cache.')
.option('--dev', 'Use development mode.')
.option('--pull-dockerhub <dockerhub-image>', 'Sets a custom Docker Hub image for base image pulls.')
.option(
'--import-tar <tar-path>',
'Load a pre-built image tar archive (e.g. ./image-v1.0.0.tar) into the enabled target(s) without building. Combine with --kind, --kubeadm, --k3s and/or --docker-compose; the archive is loaded into each enabled one.',
)
.description('Manages Docker images, including building, saving, and loading into Kubernetes clusters.')
.action(async (options) => {
if (options.rm) Underpost.image.rm({ ...options, imageName: options.rm });
if (options.ls) Underpost.image.list({ ...options, log: true });
if (options.pullBase) Underpost.image.pullBaseImages(options);
if (options.build) Underpost.image.build(options);
if (options.importTar) Underpost.image.importTar(options);
if (options.pullDockerhub)
Underpost.image.pullDockerHubImage({ ...options, dockerhubImage: options.pullDockerhub });
});
program
.command('install')
.description('Quickly imports Underpost npm dependencies by copying them.')
.action(() => {
fs.copySync(`${underpostRootPath}/node_modules`, './node_modules');
});
program
.command('db')
.argument('[deploy-list]', 'A comma-separated list of deployment IDs (e.g., "default-a,default-b").')
.option('--import', 'Imports container backups from specified repositories.')
.option('--export', 'Exports container backups to specified repositories.')
.option(
'--pod-name <pod-name>',
'Comma-separated list of pod names or patterns (supports wildcards like "mariadb-*").',
)
.option('--all-pods', 'Target all matching pods instead of just the first one.')
.option('--primary-pod', 'Automatically detect and use MongoDB primary pod (MongoDB only).')
.option('--primary-pod-ensure <pod-name>', 'Ensure setup of MongoDB replica set primary pod before operations.')
.option('--stats', 'Display database statistics (collection/table names with document/row counts).')
.option('--collections <collections>', 'Comma-separated list of database collections to operate on.')
.option('--out-path <out-path>', 'Specifies a custom output path for backups.')
.option('--drop', 'Drops the specified databases or collections before importing.')
.option('--preserveUUID', 'Preserves UUIDs during database import operations.')
.option('--git', 'Enables Git integration for backup version control (clone, pull, commit, push to GitHub).')
.option('--force-clone', 'Forces cloning of the Git repository, overwriting local changes.')
.option('--hosts <hosts>', 'Comma-separated list of database hosts to filter operations.')
.option('--paths <paths>', 'Comma-separated list of paths to filter database operations.')
.option('--ns <ns-name>', 'Kubernetes namespace context for database operations (defaults to "default").')
.option(
'--macro-rollback-export <n-commits-reset>',
'Exports a macro rollback script that reverts the last n commits (Git integration required).',
)
.option(
'--clean-fs-collection',
'Cleans orphaned File documents from collections that are not referenced by any models.',
)
.option(
'--clean-fs-dry-run',
'Dry run mode - shows what would be deleted without actually deleting (use with --clean-fs-collection).',
)
.option('--dev', 'Sets the development cli context')
.option('--kubeadm', 'Enables the kubeadm context for database operations.')
.option('--kind', 'Enables the kind context for database operations.')
.option('--k3s', 'Enables the k3s context for database operations.')
.option('--repo-backup', 'Backs up repositories (git commit+push) inside deployment pods via kubectl exec.')
.description(
'Manages database operations with support for MariaDB and MongoDB, including import/export, multi-pod targeting, and Git integration.',
)
.action(Underpost.db.callback);
program
.command('metadata')
.argument('[deploy-id]', 'The deployment ID to manage metadata.')
.argument('[host]', 'The host to manage metadata.')
.argument('[path]', 'The path to manage metadata.')
.option('--import', 'Imports from local storage.')
.option('--export', 'Exports to local storage.')
.option('--crons', 'Apply to cron data collection')
.option('--instances', 'Apply to instance data collection')
.option('--generate', 'Generate cluster metadata')
.option('--itc', 'Apply under container execution context')
.option('--dev', 'Sets the development cli context')
.description('Manages cluster metadata operations, including import and export.')
.action(Underpost.db.clusterMetadataBackupCallback);
program
.command('cron')
.argument('[deploy-list]', 'A comma-separated list of deployment IDs (e.g., "default-a,default-b").')
.argument(
'[job-list]',
`A comma-separated list of job IDs. Options: ${Underpost.cron.getJobsIDs()}. Defaults to all available jobs.`,
)
.option('--generate-k8s-cronjobs', 'Generates Kubernetes CronJob YAML manifests from cron configuration.')
.option('--apply', 'Applies generated K8s CronJob manifests to the cluster via kubectl.')
.option(
'--setup-start [deploy-id]',
'Updates deploy-id package.json start script and generates+applies its K8s CronJob manifests.',
)
.option('--namespace <namespace>', 'Kubernetes namespace for the CronJob resources (default: "default").')
.option('--image <image>', 'Custom container image for the CronJob pods.')
.option('--git', 'Pass --git flag to cron job execution.')
.option('--cmd <cmd>', 'Optional pre-script commands to run before cron execution.')
.option('--dev', 'Use local ./ base path instead of global underpost installation.')
.option('--k3s', 'Use k3s cluster context (apply directly on host).')
.option('--kind', 'Use kind cluster context (apply via kind-worker container).')
.option('--kubeadm', 'Use kubeadm cluster context (apply directly on host).')
.option('--dry-run', 'Preview cron jobs without executing them.')
.option(
'--create-job-now',
'After applying manifests, immediately create a Job from each CronJob (requires --apply).',
)
.description('Manages cron jobs: execute jobs directly or generate and apply K8s CronJob manifests.')
.action(Underpost.cron.callback);
program
.command('fs')
.argument('[path]', 'The absolute or relative directory path for file operations.')
.option('--rm', 'Removes the specified file.')
.option('--git', 'Displays current Git changes related to file storage.')
.option('--recursive', 'Uploads files recursively from the specified path.')
.option('--deploy-id <deploy-id>', 'Specifies the deployment configuration ID for file operations.')
.option('--pull', 'Downloads the specified file.')
.option('--omit-unzip', 'With --pull, keeps the downloaded .zip file and skips extraction.')
.option('--force', 'Forces the action, overriding any warnings or conflicts.')
.option('--storage-file-path <storage-file-path>', 'Specifies a custom file storage path.')
.description('Manages file storage, defaulting to file upload operations.')
.action(Underpost.fs.callback);
program
.command('test')
.argument('[deploy-list]', 'A comma-separated list of deployment IDs (e.g., "default-a,default-b").')
.description('Manages and runs tests, defaulting to the current Underpost default test suite.')
.option('--itc', 'Executes tests within the container execution context.')
.option('--sh', 'Copies the container entrypoint shell command to the clipboard.')
.option('--logs', 'Displays container logs for test debugging.')
.option('--pod-name <pod-name>', 'Optional: Specifies the pod name for test execution.')
.option('--pod-status <pod-status>', 'Optional: Filters tests by pod status.')
.option('--kind-type <kind-type>', 'Optional: Specifies the Kind cluster type for tests.')
.action(Underpost.test.callback);
program
.command('monitor')
.argument('<deploy-id>', 'The deployment configuration ID to monitor.')
.argument(
'[env]',
'Optional: The environment to monitor (e.g., "development", "production"). Defaults to "development".',
)
.option('--ms-interval <ms-interval>', 'Sets a custom millisecond interval for monitoring checks.')
.option('--now', 'Executes the monitor script immediately.')
.option('--single', 'Disables recurrence, running the monitor script only once.')
.option('--replicas <replicas>', 'Sets a custom number of replicas for monitoring. Defaults to 1.')
.option('--type <type>', 'Sets a custom monitor type.')
.option('--sync', 'Synchronizes with current proxy deployments and traffic configurations.')
.option('--namespace <namespace>', 'Sets the Kubernetes namespace for the deployment. Defaults to "default".')
.option('--timeout-response <duration>', 'Sets HTTPProxy per-route response timeout (e.g., "5s").')
.option('--timeout-idle <duration>', 'Sets HTTPProxy per-route idle timeout (e.g., "10s", "infinity").')
.option('--retry-count <count>', 'Sets HTTPProxy per-route retry count (e.g., 3).')
.option('--retry-per-try-timeout <duration>', 'Sets HTTPProxy retry per-try timeout (e.g., "150ms").')
.option('--disable-private-conf-update', 'Disables updates to private configuration during execution.')
.option('--versions <deployment-versions>', 'Specifies the deployment versions to monitor. eg. "blue,green", "green"')
.option('--ready-deployment', 'Run in ready deployment monitor mode.')
.option('--promote', 'Promotes the deployment after monitoring.')
.description('Manages health server monitoring for specified deployments.')
.action(Underpost.monitor.callback);
program
.command('ssh')
.option('--deploy-id <deploy-id>', 'Sets deploy id context for ssh operations.')
.option('--generate', 'Generates new ssh credential and stores it in current private keys file storage.')
.option('--user <user>', 'Sets custom ssh user')
.option('--password <password>', 'Sets custom ssh password')
.option('--host <host>', 'Sets custom ssh host')
.option('--port <port>', 'Sets custom ssh port')
.option('--filter <filter>', 'Filters ssh user credentials from current private keys file storage.')
.option('--groups <groups>', 'Sets comma-separated ssh user groups for the ssh user credential.')
.option('--user-add', 'Adds a new ssh user credential to current private keys file storage.')
.option('--user-remove', 'Removes an existing ssh user credential from current private keys file storage.')
.option('--user-ls', 'Lists all ssh user credentials from current private keys file storage.')
.option('--start', 'Starts an SSH session with the specified credentials.')
.option('--reset', 'Resets ssh configuration and deletes all stored credentials.')
.option('--keys-list', 'Lists all ssh keys from current private keys file storage.')
.option('--hosts-list', 'Lists all ssh hosts from current private keys file storage.')
.option('--disable-password', 'Disables password authentication for the SSH session.')
.option('--key-test', 'Tests the SSH key using ssh-keygen.')
.option('--stop', 'Stops the SSH service.')
.option('--status', 'Checks the status of the SSH service.')
.option('--connect-uri', 'Displays the connection URI.')
.option('--copy', 'Copies the connection URI to clipboard.')
.description('Manages SSH credentials and sessions for remote access to cluster nodes or services.')
.action(Underpost.ssh.callback);
program
.command('run')
.argument('<runner-id>', `The runner ID to run. Options: ${Underpost.run.RUNNERS}.`)
.argument('[path]', 'The input value, identifier, or path for the operation.')
.option('--cmd <command-list>', 'Comma-separated list of commands to execute.')
.option('--args <args-array>', 'Array of arguments to pass to the command.')
.option('--dev', 'Sets the development context environment for the script.')
.option('--build', 'Set builder context runner')
.option('--replicas <replicas>', 'Sets a custom number of replicas for deployment.')
.option('--pod-name <pod-name>', 'Optional: Specifies the pod name for execution.')
.option('--node-name <node-name>', 'Optional: Specifies the node name for execution.')
.option(
'--ssh-key-path <path>',
'Optional: Private key path for node SSH operations, forwarded to volume shipping over SSH. Defaults to engine-private/deploy/id_rsa.',
)
.option('--port <port>', 'Optional: Specifies the port for execution.')
.option('--etc-hosts', 'Enables etc-hosts context for the runner execution.')
.option('--volume-host-path <volume-host-path>', 'Optional: Specifies the volume host path for test execution.')
.option('--volume-mount-path <volume-mount-path>', 'Optional: Specifies the volume mount path for test execution.')
.option('--volume-type <volume-type>', 'Optional: Specifies the volume type for test execution.')
.option('--image-name <image-name>', 'Optional: Specifies the image name for test execution.')
.option('--container-name <container-name>', 'Optional: Specifies the container name for test execution.')
.option('--namespace <namespace>', 'Optional: Specifies the namespace for test execution.')
.option('--tty', 'Enables TTY for the container in deploy-job.')
.option('--stdin', 'Keeps STDIN open for the container in deploy-job.')
.option('--restart-policy <policy>', 'Sets the restart policy for the job in deploy-job.')
.option('--runtime-class-name <name>', 'Sets the runtime class name for the job in deploy-job.')
.option('--image-pull-policy <policy>', 'Sets the image pull policy for the job in deploy-job.')
.option('--api-version <version>', 'Sets the API version for the job manifest in deploy-job.')
.option(
'--labels <labels>',
'Optional: Specifies a comma-separated list of key-value pairs for labels (e.g., "app=my-app,env=prod").',
)
.option('--claim-name <name>', 'Optional: Specifies the claim name for volume mounting in deploy-job.')
.option(
'--kind-type <kind-type>',
'Specifies the kind of Kubernetes resource (e.g., Job, Deployment) for deploy-job.',
)
.option('--force', 'Forces operation, overriding any warnings or conflicts.')
.option('--tls', 'Enables TLS for the runner execution.')
.option('--reset', 'Resets the runner state before execution.')
.option('--dev-proxy-port-offset <port-offset>', 'Sets a custom port offset for development proxy.')
.option('--host-network', 'Enables host network mode for the runner execution.')
.option('--requests-memory <requests-memory>', 'Requests memory limit for the runner execution.')
.option('--requests-cpu <requests-cpu>', 'Requests CPU limit for the runner execution.')
.option('--limits-memory <limits-memory>', 'Sets memory limit for the runner execution.')
.option('--limits-cpu <limits-cpu>', 'Sets CPU limit for the runner execution.')
.option(
'--resource-template-id <resource-template-id >',
'Specifies a resource template ID for the runner execution.',
)
.option('--expose', 'Enables service exposure for the runner execution.')
.option('--conf-server-path <conf-server-path>', 'Sets a custom configuration server path.')
.option('--underpost-root <underpost-root>', 'Sets a custom Underpost root path.')
.option('--cmd-cron-jobs <cmd-cron-jobs>', 'Pre-script commands to run before cron job execution.')
.option(
'--deploy-id-cron-jobs <deploy-id-cron-jobs>',
'Specifies deployment IDs to synchronize cron jobs with during execution.',
)
.option('--timezone <timezone>', 'Sets the timezone for the runner execution.')
.option('--kubeadm', 'Sets the kubeadm cluster context for the runner execution.')
.option('--k3s', 'Sets the k3s cluster context for the runner execution.')
.option('--kind', 'Sets the kind cluster context for the runner execution.')
.option('--traffic <traffic>', 'Blue/green traffic colour to bake into generated manifests (default: blue).')
.option('--git-clean', 'Runs git clean on volume mount paths before copying.')
.option('--deploy-id <deploy-id>', 'Sets deploy id context for the runner execution.')
.option('--user <user>', 'Sets user context for the runner execution.')
.option('--hosts <hosts>', 'Comma-separated list of hosts for the runner execution.')
.option('--instance-id <instance-id>', 'Sets instance id context for the runner execution.')
.option('--pid <process-id>', 'Sets process id context for the runner execution.')
.option(
'--timeout-response <duration>',
'Sets HTTPProxy per-route response timeout (e.g., "1s", "300ms", "infinity").',
)
.option('--timeout-idle <duration>', 'Sets HTTPProxy per-route idle timeout (e.g., "10s", "infinity").')
.option('--retry-count <count>', 'Sets HTTPProxy per-route retry count (e.g., 3).')
.option('--retry-per-try-timeout <duration>', 'Sets HTTPProxy retry per-try timeout (e.g., "150ms").')
.option('--disable-private-conf-update', 'Disables updates to private configuration during execution.')
.option('--logs', 'Streams logs during the runner execution.')
.option('--monitor-status <status>', 'Sets the status to monitor for pod/resource (default: "Running").')
.option(
'--monitor-status-kind-type <kind-type>',
'Sets the Kubernetes resource kind type to monitor (default: "pods").',
)
.option(
'--monitor-status-delta-ms <milliseconds>',
'Sets the polling interval in milliseconds for status monitoring (default: 1000).',
)
.option(
'--monitor-status-max-attempts <attempts>',
'Sets the maximum number of status check attempts (default: 600).',
)
.option('--dry-run', 'Preview operations without executing them.')
.option(
'--from-n-commit <n>',
'Number of commits back to use for message propagation in template-deploy (default: 1, last commit only).',
)
.option(
'--create-job-now',
'After applying cron manifests, immediately create a Job from each CronJob (forwarded to cron runner).',
)
.option(
'--host-aliases <host-aliases>',
'Adds entries to the Pod /etc/hosts via hostAliases. ' +
'Format: semicolon-separated entries of "ip=hostname1,hostname2" ' +
'(e.g., "127.0.0.1=foo.local,bar.local;10.1.2.3=foo.remote,bar.remote").',
)
.option('--copy', 'Copies the runner output to the clipboard (supported by: generate-pass, template-deploy-local).')
.option(
'--skip-full-build',
'Skip client bundle rebuild; triggers pull-bundle in container startup (supported by: sync, template-deploy).',
)
.option(
'--pull-bundle',
'Explicitly download the pre-built client bundle from Cloudinary inside the container (supported by: sync, template-deploy). Use together with --skip-full-build.',
)
.option('--remove', 'Remove/teardown resources')
.option(
'--test',
'Enables test/generic-purpose mode for the runner (e.g. use self-signed TLS instead of cert-manager).',
)
.description('Runs specified scripts using various runners.')
.action(Underpost.run.callback);
program
.command('docker-compose')
.argument('[target]', 'Optional service name for --logs, --shell, --restart, or --build.')
.option('--install', 'Install Docker Engine and the Compose v2 plugin on RHEL/Rocky hosts.')
.option(
'--reset',
'Comprehensive teardown (equivalent to cluster --reset): removes all stack containers, the network, named volumes (destroys data), orphans, and generated artifacts.',
)
.option('--force', 'Force reinstall (--install), remove volumes (--down), or also drop the env-file (--reset).')
.option(
'--deploy-id <deploy-id>',
"Deployment to run as the app container (default: dd-default). 'dd-default' self-bootstraps a fresh engine; any other id runs the standard 'underpost start' command (mirrors src/cli/deploy.js).",
)
.option(
'--docker-compose-id <docker-compose-id>',
'Selects a canonical custom-workflow stack at engine-private/conf/<deploy-id>/docker-compose/<docker-compose-id>/ ' +
'(docker-compose.yml + compose.env + nginx.conf, used as-is; nginx/env generation is skipped). ' +
'e.g. --deploy-id dd-cyberia --docker-compose-id cyberia for the Cyberia MMO ecosystem.',
)
.option('--env <env>', 'Deployment environment for non-default deploy ids (default: development).')
.option('--generate', 'Render dynamic supporting files (nginx router config, env-file, app-command override).')
.option('--up', 'Start the full stack detached (regenerates config first).')
.option('--down', 'Stop and remove containers (and orphans).')
.option('--volumes', 'With --down, also remove named volumes (destroys persisted data).')
.option('--restart', 'Restart services (optionally a single [target]).')
.option('--build', 'With --up rebuild images; alone, rebuilds images with --no-cache.')
.option('--pull', 'Pull upstream images for all services.')
.option('--logs', 'Follow logs for all services (optionally a single [target]).')
.option('--status', 'Show a formatted status table of services.')
.option('--shell', 'Open an interactive shell in [target] (default: app).')
.option('--exec <subcommand>', 'General-purpose passthrough docker compose subcommand.')
.option('--compose-file <path>', 'Path to the compose file (default: docker-compose.yml).')
.option('--env-file <path>', 'Path to the compose env-file (default: docker/compose.env).')
.option('--nginx-conf <path>', 'Path to the generated nginx config (default: docker/nginx/default.conf).')
.description('General-purpose Docker Compose development pipeline (mirrors the Kubernetes dev stack).')
.action(Underpost.dockerCompose.callback);
program
.command('lxd')
.argument(
'[vm-id]',
'VM identifier shared by current-VM flags like --vm-create, --vm-delete, --vm-init, --vm-info, and --vm-test.',
)
.option('--init', 'Initializes LXD on the current machine via preseed.')
.option(
'--reset',
'Host-safe reset: removes proxy devices, stops/deletes VMs, drops admin-profile and lxdbr0. Does NOT touch the LXD snap or storage pools.',
)
.option(
'--purge',
'DESTRUCTIVE: gracefully shuts down the LXD daemon (60s timeout), then removes the LXD snap. Combine with --reset to wipe per-VM state first. Safe replacement for the prior aggressive teardown.',
)
.option(
'--shutdown',
'Pre-host-reboot procedure: gracefully stops every VM and the LXD daemon. Run BEFORE any reboot/poweroff to keep the host bootable.',
)
.option(
'--restore',
'Symmetric to --shutdown: starts the LXD daemon, waits for it to be responsive, then starts every VM. VMs created via admin-profile have boot.autostart=false, so this is the explicit "bring the lab back up" command.',
)
.option('--install', 'Installs the LXD snap.')
.option('--dev', 'Use local paths instead of the global npm installation.')
.option('--create-virtual-network', 'Creates the lxdbr0 bridge network.')
.option('--ipv4-address <cidr>', 'IPv4 address/CIDR for the lxdbr0 bridge network (default: "10.250.250.1/24").')
.option('--create-admin-profile', 'Creates the admin-profile for VM management.')
.option('--control', 'Initialize the target VM as a K3s control plane node.')
.option('--worker', 'Initialize the target VM as a K3s worker node.')
.option('--vm-create', 'Copy the LXC launch command for the command argument [vm-id] to the clipboard.')
.option(
'--vm-delete',
'SAFELY stop and delete the command argument [vm-id] (removes proxy devices first, then stops, then deletes). Safe to re-run.',
)
.option(
'--vm-init',
'Bring the command argument [vm-id] up as a K3s node end-to-end: OS base setup, mirror /home/dd/engine into the VM, then K3s role install via the local engine (use with --control or --worker).',
)
.option('--vm-info', 'Display full configuration and status for the command argument [vm-id].')
.option('--vm-test', 'Run connectivity and health checks on the command arg