@hashgraph/solo
Version:
An opinionated CLI tool to deploy and manage private Hedera Networks.
154 lines (140 loc) • 6.08 kB
text/typescript
// SPDX-License-Identifier: Apache-2.0
import {Flags as flags} from './flags.js';
import {type CommandFlag} from '../types/flag-types.js';
import {type TaskListWrapper} from '../core/task-list/task-list-wrapper.js';
import {type Listr, type ListrContext, type ListrRendererValue} from 'listr2';
import {type TaskList} from '../core/task-list/task-list.js';
import {type TaskNodeType} from '../core/task-list/task-list.js';
import {ArgumentProcessor} from '../argument-processor.js';
import {container} from 'tsyringe-neo';
import {InjectTokens} from '../core/dependency-injection/inject-tokens.js';
import {type ConfigManager} from '../core/config-manager.js';
/**
* Helper function to convert a flag object to CLI option string
* @param flag - The command flag
* @returns CLI option string (e.g., '--deployment')
*/
export function optionFromFlag(flag: CommandFlag): string {
return `--${flag.name}`;
}
/**
* Helper function to create base argv array for command execution
* @returns Base argv array
*/
export function newArgv(): string[] {
return ['${PATH}/node', '${SOLO_ROOT}/solo.ts'];
}
/**
* Helper function to append global flags to argv
* @param argv - The argument array to append to
* @param cacheDirectory - Optional cache directory path
* @returns Updated argv array
*/
export function argvPushGlobalFlags(argv: string[], cacheDirectory: string = ''): string[] {
// Only propagate flags if they are explicitly set to true in the parent command
const configManager: ConfigManager = container.resolve<ConfigManager>(InjectTokens.ConfigManager);
const developmentMode: boolean = configManager.getFlag<boolean>(flags.devMode);
if (typeof developmentMode === 'boolean' && developmentMode) {
argv.push(optionFromFlag(flags.devMode));
}
const quiet: boolean = configManager.getFlag<boolean>(flags.quiet);
if (typeof quiet === 'boolean' && quiet) {
argv.push(optionFromFlag(flags.quiet));
}
if (typeof cacheDirectory === 'string' && cacheDirectory.length > 0) {
argv.push(optionFromFlag(flags.cacheDir), cacheDirectory);
}
return argv;
}
/**
* Helper function to invoke a Solo command with proper task integration
* @param title - Task title to display
* @param commandName - Command name for task tracking
* @param callback - Function that returns the argv array
* @param taskList - TaskList instance for managing parent-child task relationships
* @param skipCallback - Optional function to determine if task should be skipped
* @returns Task object for Listr
*/
export function invokeSoloCommand(
title: string,
commandName: string,
callback: () => Promise<string[]> | string[],
taskList: TaskList<ListrContext, ListrRendererValue, ListrRendererValue>,
skipCallback?: () => boolean,
): {
title: string;
skip: () => boolean;
task: (
_context: ListrContext,
taskListWrapper: TaskListWrapper,
) => Promise<Listr<ListrContext, ListrRendererValue, ListrRendererValue>>;
} {
return {
title,
skip: skipCallback || ((): boolean => false),
task: async (
_context: ListrContext,
taskListWrapper: TaskListWrapper,
): Promise<Listr<ListrContext, ListrRendererValue, ListrRendererValue>> => {
return taskListWrapper.newListr(
[
{
title,
task: async (
_isolatedContext,
isolatedTaskWrapper,
): Promise<
| Listr<ListrContext, ListrRendererValue, ListrRendererValue>
| Listr<ListrContext, ListrRendererValue, ListrRendererValue>[]
> => {
return subTaskSoloCommand(commandName, isolatedTaskWrapper, callback, taskList);
},
},
],
{
ctx: {},
},
);
},
};
}
/**
* Helper function to execute a Solo command and return child tasks
* @param commandName - Command name for task tracking
* @param taskListWrapper - Task list wrapper from Listr
* @param callback - Function that returns the argv array
* @param taskList - TaskList instance for managing parent-child task relationships
* @returns Child tasks from command execution
*/
export async function subTaskSoloCommand(
commandName: string,
taskListWrapper: TaskListWrapper,
callback: () => Promise<string[]> | string[],
taskList: TaskList<ListrContext, ListrRendererValue, ListrRendererValue>,
): Promise<
| Listr<ListrContext, ListrRendererValue, ListrRendererValue>
| Listr<ListrContext, ListrRendererValue, ListrRendererValue>[]
> {
// one-shot can launch the same subcommand name in parallel (for example in
// nested/parallel Listr branches). A single map slot per command name caused
// last-writer-wins behavior, where one invocation overwrote another and task
// output got attached to the wrong parent. Queueing preserves 1:1 pairing.
const taskNode: TaskNodeType = {taskListWrapper};
const pendingTaskNodes: TaskNodeType[] = taskList.parentTaskListMap.get(commandName) ?? [];
pendingTaskNodes.push(taskNode);
taskList.parentTaskListMap.set(commandName, pendingTaskNodes);
const newArgv: string[] = await callback();
const configManager: ConfigManager = container.resolve<ConfigManager>(InjectTokens.ConfigManager);
const scopedConfig: ReturnType<ConfigManager['cloneActiveConfig']> = configManager.cloneActiveConfig();
// Subcommands run in parallel during one-shot flows. values-file is component-specific
// and must come from each subcommand argv, not inherited from sibling command state.
delete (scopedConfig.flags as Record<string, unknown>)[flags.valuesFile.name];
// ArgumentProcessor/command handlers read and write config flags deeply via
// ConfigManager and Flags helpers. Running under a scoped copy keeps each
// subcommand immutable from the perspective of siblings and removes shared
// global-state races while still preserving existing call signatures.
await configManager.runWithScopedConfig(scopedConfig, async (): Promise<void> => {
await ArgumentProcessor.process(newArgv);
});
return taskNode.children;
}