yargs-file-commands 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,13 @@
1
1
  import type { CommandModule } from 'yargs';
2
+ /**
3
+ * Represents a command structure with its file path and module information
4
+ * @interface Command
5
+ */
2
6
  export interface Command {
7
+ /** Full file system path to the command file */
3
8
  fullPath: string;
9
+ /** Array of path segments representing the command hierarchy */
4
10
  segments: string[];
11
+ /** The Yargs command module implementation */
5
12
  commandModule: CommandModule;
6
13
  }
@@ -1,14 +1,43 @@
1
1
  import type { CommandModule } from 'yargs';
2
2
  import type { Command } from './Command';
3
+ /**
4
+ * Represents a node in the command tree structure
5
+ * @type {CommandTreeNode}
6
+ */
3
7
  type CommandTreeNode = {
8
+ /** Name of the command segment */
4
9
  segmentName: string;
5
10
  } & ({
11
+ /** Internal node type with children */
6
12
  type: 'internal';
13
+ /** Child command nodes */
7
14
  children: CommandTreeNode[];
8
15
  } | {
16
+ /** Leaf node type with command implementation */
9
17
  type: 'leaf';
18
+ /** The command implementation */
10
19
  command: Command;
11
20
  });
21
+ /**
22
+ * Builds a tree structure from command definitions
23
+ * @param {Command[]} commands - Array of command definitions
24
+ * @returns {CommandTreeNode[]} Root nodes of the command tree
25
+ *
26
+ * @description
27
+ * Constructs a hierarchical tree structure from flat command definitions,
28
+ * preserving the command hierarchy defined by the file system structure.
29
+ */
12
30
  export declare const buildSegmentTree: (commands: Command[]) => CommandTreeNode[];
31
+ /**
32
+ * Creates a Yargs command module from a tree node
33
+ * @param {CommandTreeNode} treeNode - The tree node to convert
34
+ * @returns {CommandModule} Yargs command module
35
+ *
36
+ * @description
37
+ * Recursively converts a tree node into a Yargs command module.
38
+ * For leaf nodes, returns the actual command implementation.
39
+ * For internal nodes, creates a parent command that manages subcommands.
40
+ */
13
41
  export declare const createCommand: (treeNode: CommandTreeNode) => CommandModule;
42
+ export declare const logCommandTree: (commands: CommandTreeNode[], level?: number) => void;
14
43
  export {};
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Builds a tree structure from command definitions
3
+ * @param {Command[]} commands - Array of command definitions
4
+ * @returns {CommandTreeNode[]} Root nodes of the command tree
5
+ *
6
+ * @description
7
+ * Constructs a hierarchical tree structure from flat command definitions,
8
+ * preserving the command hierarchy defined by the file system structure.
9
+ */
1
10
  export const buildSegmentTree = (commands) => {
2
11
  const rootTreeNodes = [];
3
12
  for (const command of commands) {
@@ -5,6 +14,13 @@ export const buildSegmentTree = (commands) => {
5
14
  }
6
15
  return rootTreeNodes;
7
16
  };
17
+ /**
18
+ * Inserts a command into the tree structure at the specified depth
19
+ * @param {CommandTreeNode[]} treeNodes - Current level tree nodes
20
+ * @param {Command} command - Command to insert
21
+ * @param {number} depth - Current depth in the segment tree
22
+ * @throws {Error} When there's a conflict between directory and command names
23
+ */
8
24
  function insertIntoTree(treeNodes, command, depth) {
9
25
  // If we've processed all segments, we shouldn't be here
10
26
  if (depth >= command.segments.length) {
@@ -41,6 +57,16 @@ function insertIntoTree(treeNodes, command, depth) {
41
57
  // Recurse into children
42
58
  insertIntoTree(currentSegment.children, command, depth + 1);
43
59
  }
60
+ /**
61
+ * Creates a Yargs command module from a tree node
62
+ * @param {CommandTreeNode} treeNode - The tree node to convert
63
+ * @returns {CommandModule} Yargs command module
64
+ *
65
+ * @description
66
+ * Recursively converts a tree node into a Yargs command module.
67
+ * For leaf nodes, returns the actual command implementation.
68
+ * For internal nodes, creates a parent command that manages subcommands.
69
+ */
44
70
  export const createCommand = (treeNode) => {
45
71
  if (treeNode.type === 'leaf') {
46
72
  return treeNode.command.commandModule;
@@ -63,3 +89,11 @@ export const createCommand = (treeNode) => {
63
89
  };
64
90
  return command;
65
91
  };
92
+ export const logCommandTree = (commands, level = 0) => {
93
+ commands.forEach((command) => {
94
+ console.debug(`${' '.repeat(level) + command.segmentName}`);
95
+ if (command.type === 'internal') {
96
+ logCommandTree(command.children, level + 1);
97
+ }
98
+ });
99
+ };
@@ -1,8 +1,38 @@
1
+ /**
2
+ * Configuration options for file-based command generation
3
+ * @interface FileCommandsOptions
4
+ */
1
5
  export type FileCommandsOptions = {
6
+ /** Array of directory paths to scan for command files */
2
7
  commandDirs: string[];
8
+ /** File extensions to consider when scanning for command files */
3
9
  extensions?: string[];
10
+ /** Regular expressions for patterns to ignore when scanning directories */
4
11
  ignorePatterns?: RegExp[];
12
+ /** Logging verbosity level */
5
13
  logLevel?: 'info' | 'debug';
6
14
  };
15
+ /**
16
+ * Default configuration options for file-based commands
17
+ * @constant
18
+ * @type {Partial<FileCommandsOptions>}
19
+ */
7
20
  export declare const DefaultFileCommandsOptions: Partial<FileCommandsOptions>;
21
+ /**
22
+ * Generates a command tree structure from files in specified directories
23
+ * @async
24
+ * @param {FileCommandsOptions} options - Configuration options for command generation
25
+ * @returns {Promise<Command[]>} Array of root-level commands with their nested subcommands
26
+ *
27
+ * @description
28
+ * This function scans the specified directories for command files and builds a hierarchical
29
+ * command structure based on the file system layout. It processes files in parallel for better
30
+ * performance and supports nested commands through directory structure.
31
+ *
32
+ * The function will:
33
+ * 1. Scan all specified command directories
34
+ * 2. Process found files to extract command information
35
+ * 3. Build a tree structure based on file paths
36
+ * 4. Convert the tree into a command hierarchy
37
+ */
8
38
  export declare const fileCommands: (options: FileCommandsOptions) => Promise<import("yargs").CommandModule<{}, {}>[]>;
@@ -1,17 +1,42 @@
1
- import { buildSegmentTree, createCommand } from './buildSegmentTree.js';
1
+ import { buildSegmentTree, createCommand, logCommandTree } from './buildSegmentTree.js';
2
2
  import { importCommandFromFile } from './importCommand.js';
3
3
  import { scanDirectory } from './scanDirectory.js';
4
4
  import { segmentPath } from './segmentPath.js';
5
+ /**
6
+ * Default configuration options for file-based commands
7
+ * @constant
8
+ * @type {Partial<FileCommandsOptions>}
9
+ */
5
10
  export const DefaultFileCommandsOptions = {
11
+ /** Default file extensions to process */
6
12
  extensions: ['.js', '.ts'],
13
+ /** Default patterns to ignore when scanning directories */
7
14
  ignorePatterns: [
8
- /^[.|_].*/,
9
- /\.(?:test|spec)\.[jt]s$/,
10
- /__(?:test|spec)__/,
11
- /\.d\.ts$/
15
+ /^[.|_].*/, // Hidden files and underscore files
16
+ /\.(?:test|spec)\.[jt]s$/, // Test files
17
+ /__(?:test|spec)__/, // Test directories
18
+ /\.d\.ts$/ // TypeScript declaration files
12
19
  ],
20
+ /** Default logging level */
13
21
  logLevel: 'info'
14
22
  };
23
+ /**
24
+ * Generates a command tree structure from files in specified directories
25
+ * @async
26
+ * @param {FileCommandsOptions} options - Configuration options for command generation
27
+ * @returns {Promise<Command[]>} Array of root-level commands with their nested subcommands
28
+ *
29
+ * @description
30
+ * This function scans the specified directories for command files and builds a hierarchical
31
+ * command structure based on the file system layout. It processes files in parallel for better
32
+ * performance and supports nested commands through directory structure.
33
+ *
34
+ * The function will:
35
+ * 1. Scan all specified command directories
36
+ * 2. Process found files to extract command information
37
+ * 3. Build a tree structure based on file paths
38
+ * 4. Convert the tree into a command hierarchy
39
+ */
15
40
  export const fileCommands = async (options) => {
16
41
  const fullOptions = { ...DefaultFileCommandsOptions, ...options };
17
42
  const commands = [];
@@ -22,12 +47,15 @@ export const fileCommands = async (options) => {
22
47
  return {
23
48
  fullPath: filePath,
24
49
  segments: segmentPath(filePath, commandDir),
25
- commandModule: await importCommandFromFile(filePath, segments[segments.length - 1])
50
+ commandModule: await importCommandFromFile(filePath, segments[segments.length - 1], fullOptions)
26
51
  };
27
52
  }));
28
53
  commands.push(...rootDirCommands);
29
54
  }));
30
55
  const commandRootNodes = buildSegmentTree(commands);
56
+ if (fullOptions.logLevel === 'debug') {
57
+ logCommandTree(commandRootNodes);
58
+ }
31
59
  const rootCommands = commandRootNodes.map((node) => createCommand(node));
32
60
  return rootCommands;
33
61
  };
@@ -1,18 +1,67 @@
1
1
  import { type ArgumentsCamelCase, type CommandBuilder, type CommandModule } from 'yargs';
2
+ /**
3
+ * Represents command alias configuration
4
+ * @type {readonly string[] | string | undefined}
5
+ */
2
6
  export type CommandAlias = readonly string[] | string | undefined;
7
+ /**
8
+ * Represents command name configuration
9
+ * @type {readonly string[] | string | undefined}
10
+ */
3
11
  export type CommandName = readonly string[] | string | undefined;
12
+ /**
13
+ * Represents command deprecation configuration
14
+ * @type {boolean | string | undefined}
15
+ */
4
16
  export type CommandDeprecated = boolean | string | undefined;
17
+ /**
18
+ * Represents command description configuration
19
+ * @type {string | false | undefined}
20
+ */
5
21
  export type CommandDescribe = string | false | undefined;
22
+ /**
23
+ * Command handler function type
24
+ * @type {Function}
25
+ */
6
26
  export type CommandHandler = (args: ArgumentsCamelCase<any>) => void | Promise<any>;
27
+ /**
28
+ * Parameters for file commands configuration
29
+ * @interface FileCommandsParams
30
+ */
7
31
  export interface FileCommandsParams {
32
+ /** Root directory for command files */
8
33
  rootDir: string;
9
34
  }
35
+ /**
36
+ * Structure of a command module import
37
+ * @interface CommandImportModule
38
+ */
10
39
  export interface CommandImportModule {
40
+ /** Command aliases */
11
41
  alias?: CommandAlias;
42
+ /** Command builder function */
12
43
  builder?: CommandBuilder;
44
+ /** Command name */
13
45
  command?: CommandName;
46
+ /** Deprecation status */
14
47
  deprecated?: CommandDeprecated;
48
+ /** Command description */
15
49
  describe?: CommandDescribe;
50
+ /** Command handler function */
16
51
  handler?: CommandHandler;
17
52
  }
18
- export declare const importCommandFromFile: (filePath: string, name: string) => Promise<CommandModule<{}, {}>>;
53
+ export interface ImportCommandOptions {
54
+ logLevel?: 'info' | 'debug';
55
+ }
56
+ /**
57
+ * Imports a command module from a file
58
+ * @async
59
+ * @param {string} filePath - Path to the command file
60
+ * @param {string} name - Command name
61
+ * @returns {Promise<CommandModule>} Imported command module
62
+ *
63
+ * @description
64
+ * Dynamically imports a command file and constructs a Yargs command module.
65
+ * If no handler is provided, creates a null implementation.
66
+ */
67
+ export declare const importCommandFromFile: (filePath: string, name: string, options: ImportCommandOptions) => Promise<CommandModule<{}, {}>>;
@@ -1,7 +1,19 @@
1
1
  import {} from 'yargs';
2
- export const importCommandFromFile = async (filePath, name) => {
2
+ /**
3
+ * Imports a command module from a file
4
+ * @async
5
+ * @param {string} filePath - Path to the command file
6
+ * @param {string} name - Command name
7
+ * @returns {Promise<CommandModule>} Imported command module
8
+ *
9
+ * @description
10
+ * Dynamically imports a command file and constructs a Yargs command module.
11
+ * If no handler is provided, creates a null implementation.
12
+ */
13
+ export const importCommandFromFile = async (filePath, name, options) => {
3
14
  const handlerModule = (await import(filePath));
4
- return {
15
+ const { logLevel = 'info' } = options;
16
+ const command = {
5
17
  command: name,
6
18
  describe: handlerModule.describe,
7
19
  alias: handlerModule.alias,
@@ -12,4 +24,8 @@ export const importCommandFromFile = async (filePath, name) => {
12
24
  // null implementation
13
25
  })
14
26
  };
27
+ if (logLevel === 'debug') {
28
+ console.debug('Importing command from', filePath, 'as', name, 'with description', command.describe);
29
+ }
30
+ return command;
15
31
  };
@@ -1,5 +1,25 @@
1
+ /**
2
+ * Options for directory scanning
3
+ * @interface ScanDirectoryOptions
4
+ */
1
5
  export interface ScanDirectoryOptions {
6
+ /** Regular expressions for patterns to ignore */
2
7
  ignorePatterns?: RegExp[];
8
+ /** File extensions to include in the scan */
3
9
  extensions?: string[];
10
+ logLevel?: 'info' | 'debug';
4
11
  }
12
+ /**
13
+ * Recursively scans a directory for command files
14
+ * @async
15
+ * @param {string} dirPath - The directory path to scan
16
+ * @param {ScanDirectoryOptions} options - Scanning configuration options
17
+ * @returns {Promise<string[]>} Array of full paths to command files
18
+ *
19
+ * @description
20
+ * Performs a recursive directory scan, filtering files based on:
21
+ * - Ignore patterns (skips matching files/directories)
22
+ * - File extensions (only includes matching files)
23
+ * The scan is performed in parallel for better performance.
24
+ */
5
25
  export declare const scanDirectory: (dirPath: string, options?: ScanDirectoryOptions) => Promise<string[]>;
@@ -1,7 +1,23 @@
1
1
  import { readdir, stat } from 'fs/promises';
2
2
  import path, { join } from 'path';
3
+ /**
4
+ * Recursively scans a directory for command files
5
+ * @async
6
+ * @param {string} dirPath - The directory path to scan
7
+ * @param {ScanDirectoryOptions} options - Scanning configuration options
8
+ * @returns {Promise<string[]>} Array of full paths to command files
9
+ *
10
+ * @description
11
+ * Performs a recursive directory scan, filtering files based on:
12
+ * - Ignore patterns (skips matching files/directories)
13
+ * - File extensions (only includes matching files)
14
+ * The scan is performed in parallel for better performance.
15
+ */
3
16
  export const scanDirectory = async (dirPath, options = {}) => {
4
- const { ignorePatterns = [], extensions = [] } = options;
17
+ const { ignorePatterns = [], extensions = [], logLevel = 'info' } = options;
18
+ if (logLevel === 'debug') {
19
+ console.debug(`Inspecting directory for possible commands: ${dirPath}`);
20
+ }
5
21
  // Check if path should be ignored
6
22
  const shouldIgnore = ignorePatterns.some((pattern) => pattern.test(dirPath));
7
23
  if (shouldIgnore) {
@@ -10,12 +26,15 @@ export const scanDirectory = async (dirPath, options = {}) => {
10
26
  try {
11
27
  const entries = await readdir(dirPath);
12
28
  const nestedFilesPromises = entries.map(async (entry) => {
29
+ const fullPath = join(dirPath, entry);
13
30
  // apply ignore pattern and early return if matched
14
31
  const shouldIgnore = ignorePatterns.some((pattern) => pattern.test(entry));
15
32
  if (shouldIgnore) {
33
+ if (logLevel === 'debug') {
34
+ console.debug(`Ignoring file because of ignorePatterns match: ${fullPath}`);
35
+ }
16
36
  return [];
17
37
  }
18
- const fullPath = join(dirPath, entry);
19
38
  const stats = await stat(fullPath);
20
39
  if (stats.isDirectory()) {
21
40
  return scanDirectory(fullPath, options);
@@ -24,6 +43,9 @@ export const scanDirectory = async (dirPath, options = {}) => {
24
43
  if (!extensions.includes(extension)) {
25
44
  return [];
26
45
  }
46
+ if (logLevel === 'debug') {
47
+ console.debug(`Inspecting file as possible command: ${fullPath}`);
48
+ }
27
49
  return [fullPath];
28
50
  });
29
51
  const nestedFiles = await Promise.all(nestedFilesPromises);
@@ -1 +1,19 @@
1
+ /**
2
+ * Converts a file path into an array of command segments
3
+ * @param {string} fullPath - The complete file system path to the command file
4
+ * @param {string} baseDir - The base directory to make the path relative to
5
+ * @returns {string[]} Array of segments representing the command hierarchy
6
+ *
7
+ * @description
8
+ * This function processes a file path into command segments by:
9
+ * 1. Making the path relative to the base directory
10
+ * 2. Splitting on directory separators
11
+ * 3. Removing file extensions
12
+ * 4. Splitting on dots for nested commands
13
+ * 5. Filtering out empty segments and 'command' keyword
14
+ *
15
+ * @example
16
+ * segmentPath('/base/dir/hello/world.command.ts', '/base/dir')
17
+ * // Returns: ['hello', 'world']
18
+ */
1
19
  export declare const segmentPath: (fullPath: string, baseDir: string) => string[];
@@ -1,12 +1,30 @@
1
+ /**
2
+ * Converts a file path into an array of command segments
3
+ * @param {string} fullPath - The complete file system path to the command file
4
+ * @param {string} baseDir - The base directory to make the path relative to
5
+ * @returns {string[]} Array of segments representing the command hierarchy
6
+ *
7
+ * @description
8
+ * This function processes a file path into command segments by:
9
+ * 1. Making the path relative to the base directory
10
+ * 2. Splitting on directory separators
11
+ * 3. Removing file extensions
12
+ * 4. Splitting on dots for nested commands
13
+ * 5. Filtering out empty segments and 'command' keyword
14
+ *
15
+ * @example
16
+ * segmentPath('/base/dir/hello/world.command.ts', '/base/dir')
17
+ * // Returns: ['hello', 'world']
18
+ */
1
19
  export const segmentPath = (fullPath, baseDir) => {
2
20
  // Remove base directory and normalize slashes
3
- const relativePath = fullPath.replace(baseDir, '').replace(/^[/\\]+/, '');
21
+ const relativePath = fullPath.replace(baseDir, '').replace(/^[/\\\\]+/, '');
4
22
  // Split into path segments and filename
5
- const allSegments = relativePath.split(/[/\\]/);
23
+ const allSegments = relativePath.split(/[/\\\\]/);
6
24
  // Process all segments including filename (without extension)
7
25
  const processedSegments = allSegments
8
26
  // Remove extension from the last segment (filename)
9
- .map((segment, index, array) => index === array.length - 1 ? segment.replace(/\.[^/.]+$/, '') : segment)
27
+ .map((segment, index, array) => index === array.length - 1 ? segment.replace(/\\.[^/.]+$/, '') : segment)
10
28
  // Split segments containing periods
11
29
  .flatMap((segment) => segment.split('.'))
12
30
  // Filter out empty segments and 'command'
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,61 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'assert';
3
+ import { segmentPath } from './segmentPath.js';
4
+ await test('segmentPath', async () => {
5
+ await test('should segment a path correctly', () => {
6
+ const fullPath = '/Users/username/Coding/Personal/yargs-file-commands/packages/yargs-file-commands/src/lib/segmentPath.ts';
7
+ const baseDir = '/Users/username/Coding/Personal/yargs-file-commands/';
8
+ const expected = [
9
+ 'packages',
10
+ 'yargs-file-commands',
11
+ 'src',
12
+ 'lib',
13
+ 'segmentPath',
14
+ 'ts'
15
+ ];
16
+ const result = segmentPath(fullPath, baseDir);
17
+ assert.deepStrictEqual(result, expected);
18
+ });
19
+ await test('should handle paths with periods correctly', () => {
20
+ const fullPath = '/Users/username/Coding/Personal/yargs-file-commands/packages/yargs-file-commands/src/lib/segmentPath.test.ts';
21
+ const baseDir = '/Users/username/Coding/Personal/yargs-file-commands/';
22
+ const expected = [
23
+ 'packages',
24
+ 'yargs-file-commands',
25
+ 'src',
26
+ 'lib',
27
+ 'segmentPath',
28
+ 'test',
29
+ 'ts'
30
+ ];
31
+ const result = segmentPath(fullPath, baseDir);
32
+ assert.deepStrictEqual(result, expected);
33
+ });
34
+ await test('should filter out "command" segments', () => {
35
+ const fullPath = '/Users/username/Coding/Personal/yargs-file-commands/packages/yargs-file-commands/src/lib/commandPath.ts';
36
+ const baseDir = '/Users/username/Coding/Personal/yargs-file-commands/';
37
+ const expected = [
38
+ 'packages',
39
+ 'yargs-file-commands',
40
+ 'src',
41
+ 'lib',
42
+ 'commandPath',
43
+ 'ts'
44
+ ];
45
+ const result = segmentPath(fullPath, baseDir);
46
+ assert.deepStrictEqual(result, expected);
47
+ });
48
+ test('should handle empty segments correctly', () => {
49
+ const fullPath = '/Users/username/Coding/Personal/yargs-file-commands/packages/yargs-file-commands/src/lib/.hiddenFile';
50
+ const baseDir = '/Users/username/Coding/Personal/yargs-file-commands/';
51
+ const expected = [
52
+ 'packages',
53
+ 'yargs-file-commands',
54
+ 'src',
55
+ 'lib',
56
+ 'hiddenFile'
57
+ ];
58
+ const result = segmentPath(fullPath, baseDir);
59
+ assert.deepStrictEqual(result, expected);
60
+ });
61
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,70 @@
1
+ import assert from 'node:assert/strict';
2
+ import { describe, it } from 'node:test';
3
+ import { buildSegmentTree } from '../lib/buildSegmentTree.js';
4
+ describe('buildSegmentTree', async () => {
5
+ it('should build correct tree structure', async () => {
6
+ const commands = [
7
+ {
8
+ fullPath: '/commands/db/migration/command.js',
9
+ segments: ['db', 'migration'],
10
+ commandModule: {
11
+ command: 'migration',
12
+ describe: 'Migration command',
13
+ handler: async () => {
14
+ // Test handler
15
+ }
16
+ }
17
+ },
18
+ {
19
+ fullPath: '/commands/db/health.js',
20
+ segments: ['db', 'health'],
21
+ commandModule: {
22
+ command: 'health',
23
+ describe: 'Health command',
24
+ handler: async () => {
25
+ // Test handler
26
+ }
27
+ }
28
+ }
29
+ ];
30
+ const tree = buildSegmentTree(commands);
31
+ assert.equal(tree.length, 1, 'Should have one root node');
32
+ const rootNode = tree[0];
33
+ assert(rootNode, 'Root node should exist');
34
+ assert.equal(rootNode.segmentName, 'db', 'Root node should be "db"');
35
+ assert.equal(rootNode.type, 'internal', 'Root node should be internal type');
36
+ if (rootNode.type !== 'internal') {
37
+ throw new Error('Expected internal node');
38
+ }
39
+ assert.equal(rootNode.children.length, 2, 'Should have two child nodes');
40
+ const childSegments = rootNode.children
41
+ .map((child) => child.segmentName)
42
+ .sort();
43
+ assert.deepEqual(childSegments, ['health', 'migration'], 'Should have "health" and "migration" sub-commands');
44
+ });
45
+ it('should handle empty input', async () => {
46
+ const tree = buildSegmentTree([]);
47
+ assert.equal(tree.length, 0, 'Should return empty array for empty input');
48
+ });
49
+ it('should handle single command', async () => {
50
+ const commands = [
51
+ {
52
+ fullPath: '/commands/test.ts',
53
+ segments: ['test'],
54
+ commandModule: {
55
+ command: 'test',
56
+ describe: 'Test command',
57
+ handler: async () => {
58
+ // Test handler
59
+ }
60
+ }
61
+ }
62
+ ];
63
+ const tree = buildSegmentTree(commands);
64
+ assert.equal(tree.length, 1, 'Should have one node');
65
+ const node = tree[0];
66
+ assert(node, 'Node should exist');
67
+ assert.equal(node.segmentName, 'test', 'Should have correct segment');
68
+ assert.equal(node.type, 'leaf', 'Should be a leaf node');
69
+ });
70
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import assert from 'node:assert/strict';
2
+ import path from 'node:path';
3
+ import { describe, it } from 'node:test';
4
+ import { fileURLToPath } from 'node:url';
5
+ import yargs from 'yargs';
6
+ import { hideBin } from 'yargs/helpers';
7
+ import { fileCommands } from '../lib/fileCommands.js';
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ describe('fileCommands', async () => {
10
+ it('should load commands from directory structure', async () => {
11
+ const commandsDir = path.join(__dirname, 'fixtures', 'commands');
12
+ const commands = await fileCommands({
13
+ commandDirs: [commandsDir],
14
+ extensions: ['.js'],
15
+ logLevel: 'debug'
16
+ });
17
+ assert.equal(commands.length, 1, 'Should have one root command');
18
+ const rootCommand = commands[0];
19
+ assert(rootCommand, 'Root command should exist');
20
+ assert.equal(rootCommand.command, 'db', 'Root command should be "db"');
21
+ // Create a new yargs instance
22
+ const yargsInstance = yargs(hideBin(process.argv));
23
+ if (typeof rootCommand.builder === 'function') {
24
+ rootCommand.builder(yargsInstance);
25
+ }
26
+ // Check that the command has subcommands by checking its description
27
+ const description = rootCommand.describe;
28
+ assert(typeof description === 'string' && description.includes('db'), 'Command should have correct description');
29
+ });
30
+ it('should respect ignore patterns', async () => {
31
+ const commandsDir = path.join(__dirname, 'fixtures', 'commands');
32
+ const commands = await fileCommands({
33
+ commandDirs: [commandsDir],
34
+ extensions: ['.js'],
35
+ ignorePatterns: [/health/],
36
+ logLevel: 'debug'
37
+ });
38
+ assert.equal(commands.length, 1, 'Should have one root command');
39
+ const rootCommand = commands[0];
40
+ assert(rootCommand, 'Root command should exist');
41
+ assert.equal(rootCommand.command, 'db', 'Root command should be "db"');
42
+ });
43
+ });
@@ -0,0 +1,3 @@
1
+ export declare const describe = "Database health check";
2
+ export declare const builder: (yargs: any) => any;
3
+ export declare const handler: () => Promise<void>;
@@ -0,0 +1,7 @@
1
+ export const describe = 'Database health check';
2
+ export const builder = (yargs) => {
3
+ return yargs;
4
+ };
5
+ export const handler = async () => {
6
+ console.log('Health check handler called');
7
+ };
@@ -0,0 +1,3 @@
1
+ export declare const describe = "Database migration command";
2
+ export declare const builder: (yargs: any) => any;
3
+ export declare const handler: (argv: any) => Promise<void>;
@@ -0,0 +1,10 @@
1
+ export const describe = 'Database migration command';
2
+ export const builder = (yargs) => {
3
+ return yargs.option('force', {
4
+ type: 'boolean',
5
+ describe: 'Force migration'
6
+ });
7
+ };
8
+ export const handler = async (argv) => {
9
+ console.log('Migration handler called');
10
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import assert from 'node:assert/strict';
2
+ import path from 'node:path';
3
+ import { describe, it } from 'node:test';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { importCommandFromFile } from '../lib/importCommand.js';
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ describe('importCommandFromFile', async () => {
8
+ it('should import command module correctly', async () => {
9
+ const commandPath = path.join(__dirname, 'fixtures', 'commands', 'db', 'health.js');
10
+ const command = await importCommandFromFile(commandPath, 'health', {});
11
+ assert(command.describe, 'Should have describe property');
12
+ assert(typeof command.builder === 'function', 'Should have builder function');
13
+ assert(typeof command.handler === 'function', 'Should have handler function');
14
+ });
15
+ it('should handle non-existent files', async () => {
16
+ const nonExistentPath = path.join(__dirname, 'fixtures', 'commands', 'non-existent.ts');
17
+ await assert.rejects(async () => {
18
+ await importCommandFromFile(nonExistentPath, 'non-existent', {});
19
+ }, Error, 'Should throw error for non-existent file');
20
+ });
21
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,41 @@
1
+ import assert from 'node:assert/strict';
2
+ import path from 'node:path';
3
+ import { describe, it } from 'node:test';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { scanDirectory } from '../lib/scanDirectory.js';
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ describe('scanDirectory', async () => {
8
+ it('should find all command files in directory', async () => {
9
+ const commandsDir = path.join(__dirname, 'fixtures', 'commands');
10
+ const files = await scanDirectory(commandsDir, {
11
+ extensions: ['.js'],
12
+ ignorePatterns: []
13
+ });
14
+ assert.equal(files.length, 2, 'Should find two command files');
15
+ assert(files.some((f) => f.includes('health.js')), 'Should find health.js');
16
+ assert(files.some((f) => f.includes('command.js')), 'Should find command.js');
17
+ });
18
+ it('should respect ignore patterns', async () => {
19
+ const commandsDir = path.join(__dirname, 'fixtures', 'commands');
20
+ const files = await scanDirectory(commandsDir, {
21
+ extensions: ['.js'],
22
+ ignorePatterns: [/health/]
23
+ });
24
+ assert.equal(files.length, 1, 'Should find one command file');
25
+ assert(files.some((f) => f.includes('command.js')), 'Should find command.js');
26
+ assert(!files.some((f) => f.includes('health.js')), 'Should not find health.js');
27
+ });
28
+ it('should handle non-existent directories', async () => {
29
+ const nonExistentDir = path.join(__dirname, 'fixtures', 'non-existent');
30
+ try {
31
+ await scanDirectory(nonExistentDir, {
32
+ extensions: ['.js']
33
+ });
34
+ assert.fail('Should have thrown an error');
35
+ }
36
+ catch (error) {
37
+ assert(error instanceof Error);
38
+ assert(error.message.includes('ENOENT'), 'Error should indicate directory not found');
39
+ }
40
+ });
41
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "yargs-file-commands",
3
3
  "description": "A yargs helper function that lets you define your commands structure via directory and file naming conventions.",
4
- "version": "0.0.3",
4
+ "version": "0.0.5",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -19,6 +19,13 @@
19
19
  "nodejs",
20
20
  "typescript"
21
21
  ],
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "typecheck": "tsc --noEmit",
25
+ "test": "tsc && node --test dist/**/*.test.js",
26
+ "lint": "eslint --fix \"src/**/*.{ts,tsx}\"",
27
+ "format": "prettier \"src/**/*.{js,jsx,css,md,html,ts,tsx,json,yaml}\" --check"
28
+ },
22
29
  "homepage": "https://github.com/bhouston/yargs-file-commands#readme",
23
30
  "repository": {
24
31
  "type": "git",
@@ -38,7 +45,8 @@
38
45
  },
39
46
  "devDependencies": {
40
47
  "@types/node": "^22.10.0",
41
- "@types/yargs": "^17.0.33"
48
+ "@types/yargs": "^17.0.33",
49
+ "yargs": "^17.0.0"
42
50
  },
43
51
  "publishConfig": {
44
52
  "registry": "https://registry.npmjs.org/",