yargs-file-commands 0.0.7 → 0.0.9

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,8 +1,8 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { describe, it } from 'node:test';
3
- import { buildSegmentTree } from '../lib/buildSegmentTree.js';
3
+ import { buildSegmentTree } from './buildSegmentTree.js';
4
4
  describe('buildSegmentTree', async () => {
5
- it('should build correct tree structure', async () => {
5
+ it('should build correct tree structure', () => {
6
6
  const commands = [
7
7
  {
8
8
  fullPath: '/commands/db/migration/command.js',
@@ -42,11 +42,11 @@ describe('buildSegmentTree', async () => {
42
42
  .sort();
43
43
  assert.deepEqual(childSegments, ['health', 'migration'], 'Should have "health" and "migration" sub-commands');
44
44
  });
45
- it('should handle empty input', async () => {
45
+ it('should handle empty input', () => {
46
46
  const tree = buildSegmentTree([]);
47
47
  assert.equal(tree.length, 0, 'Should return empty array for empty input');
48
48
  });
49
- it('should handle single command', async () => {
49
+ it('should handle single command', () => {
50
50
  const commands = [
51
51
  {
52
52
  fullPath: '/commands/test.ts',
@@ -1,3 +1,4 @@
1
+ import path from 'path';
1
2
  import { buildSegmentTree, createCommand, logCommandTree } from './buildSegmentTree.js';
2
3
  import { importCommandFromFile } from './importCommand.js';
3
4
  import { scanDirectory } from './scanDirectory.js';
@@ -38,23 +39,30 @@ export const DefaultFileCommandsOptions = {
38
39
  * 4. Convert the tree into a command hierarchy
39
40
  */
40
41
  export const fileCommands = async (options) => {
41
- const fullOptions = { ...DefaultFileCommandsOptions, ...options };
42
+ const fullOptions = {
43
+ ...DefaultFileCommandsOptions,
44
+ ...options,
45
+ logPrefix: ' '
46
+ };
42
47
  const commands = [];
43
- await Promise.all(options.commandDirs.map(async (commandDir) => {
44
- const filePaths = await scanDirectory(commandDir, fullOptions);
45
- const rootDirCommands = await Promise.all(filePaths.map(async (filePath) => {
48
+ for (const commandDir of fullOptions.commandDirs) {
49
+ const fullPath = path.resolve(commandDir);
50
+ console.debug(`Scanning directory for commands: ${fullPath}`);
51
+ const filePaths = await scanDirectory(commandDir, commandDir, fullOptions);
52
+ for (const filePath of filePaths) {
46
53
  const segments = segmentPath(filePath, commandDir);
47
- return {
54
+ segments.pop(); // remove extension.
55
+ commands.push({
48
56
  fullPath: filePath,
49
- segments: segmentPath(filePath, commandDir),
57
+ segments,
50
58
  commandModule: await importCommandFromFile(filePath, segments[segments.length - 1], fullOptions)
51
- };
52
- }));
53
- commands.push(...rootDirCommands);
54
- }));
59
+ });
60
+ }
61
+ }
55
62
  const commandRootNodes = buildSegmentTree(commands);
56
63
  if (fullOptions.logLevel === 'debug') {
57
- logCommandTree(commandRootNodes);
64
+ console.debug('Command tree structure:');
65
+ logCommandTree(commandRootNodes, 1);
58
66
  }
59
67
  const rootCommands = commandRootNodes.map((node) => createCommand(node));
60
68
  return rootCommands;
@@ -7,7 +7,7 @@ import { hideBin } from 'yargs/helpers';
7
7
  import { fileCommands } from '../lib/fileCommands.js';
8
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
9
  describe('fileCommands', async () => {
10
- it('should load commands from directory structure', async () => {
10
+ await it('should load commands from directory structure', async () => {
11
11
  const commandsDir = path.join(__dirname, 'fixtures', 'commands');
12
12
  const commands = await fileCommands({
13
13
  commandDirs: [commandsDir],
@@ -27,7 +27,7 @@ describe('fileCommands', async () => {
27
27
  const description = rootCommand.describe;
28
28
  assert(typeof description === 'string' && description.includes('db'), 'Command should have correct description');
29
29
  });
30
- it('should respect ignore patterns', async () => {
30
+ await it('should respect ignore patterns', async () => {
31
31
  const commandsDir = path.join(__dirname, 'fixtures', 'commands');
32
32
  const commands = await fileCommands({
33
33
  commandDirs: [commandsDir],
@@ -5,14 +5,14 @@ import { fileURLToPath } from 'node:url';
5
5
  import { importCommandFromFile } from '../lib/importCommand.js';
6
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
7
  describe('importCommandFromFile', async () => {
8
- it('should import command module correctly', async () => {
8
+ await it('should import command module correctly', async () => {
9
9
  const commandPath = path.join(__dirname, 'fixtures', 'commands', 'db', 'health.js');
10
10
  const command = await importCommandFromFile(commandPath, 'health', {});
11
11
  assert(command.describe, 'Should have describe property');
12
12
  assert(typeof command.builder === 'function', 'Should have builder function');
13
13
  assert(typeof command.handler === 'function', 'Should have handler function');
14
14
  });
15
- it('should handle non-existent files', async () => {
15
+ await it('should handle non-existent files', async () => {
16
16
  const nonExistentPath = path.join(__dirname, 'fixtures', 'commands', 'non-existent.ts');
17
17
  await assert.rejects(async () => {
18
18
  await importCommandFromFile(nonExistentPath, 'non-existent', {});
@@ -8,6 +8,7 @@ export interface ScanDirectoryOptions {
8
8
  /** File extensions to include in the scan */
9
9
  extensions?: string[];
10
10
  logLevel?: 'info' | 'debug';
11
+ logPrefix?: string;
11
12
  }
12
13
  /**
13
14
  * Recursively scans a directory for command files
@@ -22,4 +23,4 @@ export interface ScanDirectoryOptions {
22
23
  * - File extensions (only includes matching files)
23
24
  * The scan is performed in parallel for better performance.
24
25
  */
25
- export declare const scanDirectory: (dirPath: string, options?: ScanDirectoryOptions) => Promise<string[]>;
26
+ export declare const scanDirectory: (dirPath: string, commandDir: string, options?: ScanDirectoryOptions) => Promise<string[]>;
@@ -13,52 +13,50 @@ import path, { join } from 'path';
13
13
  * - File extensions (only includes matching files)
14
14
  * The scan is performed in parallel for better performance.
15
15
  */
16
- export const scanDirectory = async (dirPath, options = {}) => {
17
- const { ignorePatterns = [], extensions = [], logLevel = 'info' } = options;
18
- const rootedDirPath = path.resolve(dirPath);
19
- if (logLevel === 'debug') {
20
- console.debug(`Inspecting directory for possible commands: ${rootedDirPath}`);
21
- }
22
- // Check if path should be ignored
23
- const shouldIgnore = ignorePatterns.some((pattern) => pattern.test(dirPath));
24
- if (shouldIgnore) {
25
- if (logLevel === 'debug') {
26
- console.debug(`Ignoring directory because of ignorePatterns match: ${rootedDirPath}`);
27
- }
28
- return [];
29
- }
16
+ export const scanDirectory = async (dirPath, commandDir, options = {}) => {
17
+ const { ignorePatterns = [], extensions = ['.js', '.ts'], logLevel = 'info', logPrefix = '' } = options;
30
18
  try {
31
19
  const entries = await readdir(dirPath);
32
- if (logLevel === 'debug') {
33
- console.log(`Found ${entries.length} items in current directory`);
34
- }
35
- const nestedFilesPromises = entries.map(async (entry) => {
20
+ const commandPaths = [];
21
+ for (const entry of entries) {
36
22
  const fullPath = join(dirPath, entry);
23
+ const localPath = fullPath.replace(commandDir, '');
37
24
  // apply ignore pattern and early return if matched
38
- const shouldIgnore = ignorePatterns.some((pattern) => pattern.test(entry));
25
+ const shouldIgnore = ignorePatterns.some((pattern) => pattern.test(localPath));
39
26
  if (shouldIgnore) {
40
27
  if (logLevel === 'debug') {
41
- console.debug(`Ignoring file because of ignorePatterns match: ${path.resolve(fullPath)}`);
28
+ console.debug(`${logPrefix}${localPath} - ignoring because it matches ignorePattern: ${ignorePatterns
29
+ .filter((pattern) => pattern.test(localPath))
30
+ .join(', ')}`);
42
31
  }
43
- return [];
32
+ continue;
44
33
  }
45
34
  const stats = await stat(fullPath);
46
35
  if (stats.isDirectory()) {
47
- return scanDirectory(fullPath, options);
36
+ if (logLevel === 'debug') {
37
+ console.debug(`${logPrefix}${localPath} - directory, scanning for commands:`);
38
+ }
39
+ commandPaths.push(...(await scanDirectory(fullPath, commandDir, {
40
+ ...options,
41
+ logPrefix: `${logPrefix} `
42
+ })));
43
+ continue;
48
44
  }
49
45
  const extension = path.extname(fullPath);
50
46
  if (!extensions.includes(extension)) {
51
- return [];
47
+ if (logLevel === 'debug') {
48
+ console.debug(`${logPrefix}${localPath} - ignoring as its extension, ${extension}, doesn't match required extension: ${extensions.join(', ')}`);
49
+ }
50
+ continue;
52
51
  }
53
52
  if (logLevel === 'debug') {
54
- console.debug(`Inspecting file as possible command: ${fullPath}`);
53
+ console.debug(`${logPrefix}${localPath} - possible command file`);
55
54
  }
56
- return [fullPath];
57
- });
58
- const nestedFiles = await Promise.all(nestedFilesPromises);
59
- return nestedFiles.flat();
55
+ commandPaths.push(fullPath);
56
+ }
57
+ return commandPaths;
60
58
  }
61
59
  catch (error) {
62
- throw new Error(`Failed to scan directory ${dirPath}: ${error}`);
60
+ throw new Error(`${logPrefix}Failed to scan directory ${dirPath}: ${error}`);
63
61
  }
64
62
  };
@@ -0,0 +1,46 @@
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
+ await it('should find all command files in directory', async () => {
9
+ const commandsDir = path.join(__dirname, 'fixtures', 'commands');
10
+ console.log('Scan Directory: ', commandsDir);
11
+ const files = await scanDirectory(commandsDir, commandsDir, {
12
+ extensions: ['.js'],
13
+ logLevel: 'debug'
14
+ });
15
+ assert.equal(files.length, 2, `Should find two command files, instead found: ${files.join(', ')}`);
16
+ assert(files.some((f) => f.includes('health.js')), `Should find health.js, instead found: ${files.join(', ')}`);
17
+ assert(files.some((f) => f.includes('command.js')), `Should find command.js, instead found: ${files.join(', ')}`);
18
+ });
19
+ await it('should respect ignore patterns', async () => {
20
+ const commandsDir = path.join(__dirname, 'fixtures', 'commands');
21
+ console.log('Scan Directory: ', commandsDir);
22
+ const files = await scanDirectory(commandsDir, commandsDir, {
23
+ extensions: ['.js'],
24
+ ignorePatterns: [/health/],
25
+ logLevel: 'debug'
26
+ });
27
+ assert.equal(files.length, 1, `Should find one command file, instead found: ${files.join(', ')}`);
28
+ assert(files.some((f) => f.includes('command.js')), `Should find command.js, instead found: ${files.join(', ')}`);
29
+ assert(!files.some((f) => f.includes('health.js')), `Should not find health.js, instead found: ${files.join(', ')}`);
30
+ });
31
+ await it('should handle non-existent directories', async () => {
32
+ const nonExistentDir = path.join(__dirname, 'fixtures', 'non-existent');
33
+ try {
34
+ console.log('Scan Directory: ', nonExistentDir);
35
+ await scanDirectory(nonExistentDir, nonExistentDir, {
36
+ extensions: ['.js'],
37
+ logLevel: 'debug'
38
+ });
39
+ assert.fail('Should have thrown an error');
40
+ }
41
+ catch (error) {
42
+ assert(error instanceof Error);
43
+ assert(error.message.includes('ENOENT'), 'Error should indicate directory not found');
44
+ }
45
+ });
46
+ });
@@ -1,8 +1,8 @@
1
- import { test } from 'node:test';
1
+ import { describe, it } from 'node:test';
2
2
  import assert from 'assert';
3
3
  import { segmentPath } from './segmentPath.js';
4
- await test('segmentPath', async () => {
5
- await test('should segment a path correctly', () => {
4
+ describe('segmentPath', () => {
5
+ it('should segment a path correctly', () => {
6
6
  const fullPath = '/Users/username/Coding/Personal/yargs-file-commands/packages/yargs-file-commands/src/lib/segmentPath.ts';
7
7
  const baseDir = '/Users/username/Coding/Personal/yargs-file-commands/';
8
8
  const expected = [
@@ -16,7 +16,7 @@ await test('segmentPath', async () => {
16
16
  const result = segmentPath(fullPath, baseDir);
17
17
  assert.deepStrictEqual(result, expected);
18
18
  });
19
- await test('should handle paths with periods correctly', () => {
19
+ it('should handle paths with periods correctly', () => {
20
20
  const fullPath = '/Users/username/Coding/Personal/yargs-file-commands/packages/yargs-file-commands/src/lib/segmentPath.test.ts';
21
21
  const baseDir = '/Users/username/Coding/Personal/yargs-file-commands/';
22
22
  const expected = [
@@ -31,7 +31,7 @@ await test('segmentPath', async () => {
31
31
  const result = segmentPath(fullPath, baseDir);
32
32
  assert.deepStrictEqual(result, expected);
33
33
  });
34
- await test('should filter out "command" segments', () => {
34
+ it('should filter out "command" segments', () => {
35
35
  const fullPath = '/Users/username/Coding/Personal/yargs-file-commands/packages/yargs-file-commands/src/lib/commandPath.ts';
36
36
  const baseDir = '/Users/username/Coding/Personal/yargs-file-commands/';
37
37
  const expected = [
@@ -45,7 +45,7 @@ await test('segmentPath', async () => {
45
45
  const result = segmentPath(fullPath, baseDir);
46
46
  assert.deepStrictEqual(result, expected);
47
47
  });
48
- test('should handle empty segments correctly', () => {
48
+ it('should handle empty segments correctly', () => {
49
49
  const fullPath = '/Users/username/Coding/Personal/yargs-file-commands/packages/yargs-file-commands/src/lib/.hiddenFile';
50
50
  const baseDir = '/Users/username/Coding/Personal/yargs-file-commands/';
51
51
  const expected = [
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.7",
4
+ "version": "0.0.9",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -19,13 +19,6 @@
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
- },
29
22
  "homepage": "https://github.com/bhouston/yargs-file-commands#readme",
30
23
  "repository": {
31
24
  "type": "git",
@@ -54,5 +47,12 @@
54
47
  },
55
48
  "engines": {
56
49
  "node": ">=20.0.0"
50
+ },
51
+ "scripts": {
52
+ "build": "tsc",
53
+ "typecheck": "tsc --noEmit",
54
+ "test": "tsc && node --test dist/**/*.test.js --test-concurrency=1",
55
+ "lint": "eslint --fix \"src/**/*.{ts,tsx}\"",
56
+ "format": "prettier \"src/**/*.{js,jsx,css,md,html,ts,tsx,json,yaml}\" --check"
57
57
  }
58
- }
58
+ }
@@ -1,41 +0,0 @@
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
- });
File without changes
File without changes
File without changes
File without changes