yargs-file-commands
Version:
A yargs helper function that lets you define your commands structure via directory and file naming conventions.
150 lines (134 loc) • 4.42 kB
text/typescript
import type { Argv, CommandModule } from 'yargs';
import type { Command } from './Command';
/**
* Represents a node in the command tree structure
* @type {CommandTreeNode}
*/
type CommandTreeNode = {
/** Name of the command segment */
segmentName: string;
} & (
| {
/** Internal node type with children */
type: 'internal';
/** Child command nodes */
children: CommandTreeNode[];
}
| {
/** Leaf node type with command implementation */
type: 'leaf';
/** The command implementation */
command: Command;
}
);
/**
* Builds a tree structure from command definitions
* @param {Command[]} commands - Array of command definitions
* @returns {CommandTreeNode[]} Root nodes of the command tree
*
* @description
* Constructs a hierarchical tree structure from flat command definitions,
* preserving the command hierarchy defined by the file system structure.
*/
export const buildSegmentTree = (commands: Command[]): CommandTreeNode[] => {
const rootTreeNodes: CommandTreeNode[] = [];
for (const command of commands) {
insertIntoTree(rootTreeNodes, command, 0);
}
return rootTreeNodes;
};
/**
* Inserts a command into the tree structure at the specified depth
* @param {CommandTreeNode[]} treeNodes - Current level tree nodes
* @param {Command} command - Command to insert
* @param {number} depth - Current depth in the segment tree
* @throws {Error} When there's a conflict between directory and command names
*/
function insertIntoTree(
treeNodes: CommandTreeNode[],
command: Command,
depth: number
): void {
// If we've processed all segments, we shouldn't be here
if (depth >= command.segments.length) {
return;
}
const currentSegmentName = command.segments[depth]!;
let currentSegment = treeNodes.find(
(s) => s.segmentName === currentSegmentName
);
// If this is the last segment, create a leaf node
if (depth === command.segments.length - 1) {
if (currentSegment == null) {
treeNodes.push({
type: 'leaf',
segmentName: currentSegmentName,
command
});
} else if (currentSegment.type === 'internal') {
throw new Error(
`Conflict: ${currentSegmentName} is both a directory and a command ${JSON.stringify(
currentSegment
)},${JSON.stringify(command)}`
);
}
return;
}
// Creating or ensuring we have an internal node
if (currentSegment == null) {
currentSegment = {
type: 'internal',
segmentName: currentSegmentName,
children: []
};
treeNodes.push(currentSegment);
} else if (currentSegment.type === 'leaf') {
throw new Error(
`Conflict: ${currentSegmentName} is both a directory and a command ${JSON.stringify(
currentSegment
)}, ${JSON.stringify(command)}`
);
}
// Recurse into children
insertIntoTree(currentSegment.children, command, depth + 1);
}
/**
* Creates a Yargs command module from a tree node
* @param {CommandTreeNode} treeNode - The tree node to convert
* @returns {CommandModule} Yargs command module
*
* @description
* Recursively converts a tree node into a Yargs command module.
* For leaf nodes, returns the actual command implementation.
* For internal nodes, creates a parent command that manages subcommands.
*/
export const createCommand = (treeNode: CommandTreeNode): CommandModule => {
if (treeNode.type === 'leaf') {
return treeNode.command.commandModule;
}
const name = treeNode.segmentName;
// For internal nodes, create a command that registers all children
const command: CommandModule = {
command: name,
describe: `${name} commands`,
builder: (yargs: Argv): Argv => {
// Register all child segments as subcommands
yargs.command(treeNode.children.map((child) => createCommand(child)));
// Demand a subcommand unless we're at the root
yargs.demandCommand(1, `You must specify a ${name} subcommand`);
return yargs;
},
handler: async () => {
// Internal nodes don't need handlers as they'll demand subcommands
}
};
return command;
};
export const logCommandTree = (commands: CommandTreeNode[], level = 0) => {
commands.forEach((command) => {
console.debug(`${' '.repeat(level) + command.segmentName}`);
if (command.type === 'internal') {
logCommandTree(command.children, level + 1);
}
});
};