yargs-file-commands 0.0.14 → 0.0.17

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # yargs-file-commands
2
2
 
3
+ ## 0.0.17
4
+
5
+ ### Patch Changes
6
+
7
+ - Throw if a command exports unrecognized names, catches a lot of bugs.
8
+ - More explicit exceptions when bad parameters are passed in
9
+ - Validate that provided command directories are aboslute
10
+ - More robust parameter checking and logging
11
+ - Improved debugging logging
12
+ - added support for default commands and optional command names to support position arguments
13
+ - More robust debug messages
14
+
15
+ ## 0.0.15
16
+
17
+ ### Patch Changes
18
+
19
+ - Throw if a command exports unrecognized names, catches a lot of bugs.
20
+ - More explicit exceptions when bad parameters are passed in
21
+ - Validate that provided command directories are aboslute
22
+ - More robust parameter checking and logging
23
+ - Improved debugging logging
24
+ - More robust debug messages
25
+
3
26
  ## 0.0.14
4
27
 
5
28
  ### Patch Changes
package/README.md CHANGED
@@ -39,13 +39,14 @@ You can use any combination of file names and directories. We support either [Ne
39
39
  │ ├── migration
40
40
  │ │ └── command.ts // the "db migration" command
41
41
  │ └── health.ts // the "db health" command
42
+ ├── $default.ts // the default command
42
43
  └── studio.start.ts // the "studio start" command
43
44
  ```
44
45
 
45
- Inside each route handler file, you make the default export the route handler. Here is a simple example:
46
+ Inside each route handler file, you define your command configuration. The command name defaults to the filename, but you can explicitly specify it using the `command` export to support positional arguments. Here are some examples:
46
47
 
47
48
  ```ts
48
- // commands/studio.start.ts
49
+ // commands/studio.start.ts - Basic command using filename as command name
49
50
 
50
51
  import type { ArgumentsCamelCase, Argv } from 'yargs';
51
52
  import type { BaseOptions } from '../options.js';
@@ -54,6 +55,8 @@ export interface Options extends BaseOptions {
54
55
  port?: number;
55
56
  }
56
57
 
58
+ export const command = 'start'; // this is optional, it will use the filename if this isn't specified
59
+
57
60
  export const describe = 'Studio web interface';
58
61
 
59
62
  export const builder = (args: Argv): Argv<Options> => {
@@ -71,6 +74,35 @@ export const handler = async (args: ArgumentsCamelCase<Options>) => {
71
74
  };
72
75
  ```
73
76
 
77
+ ```ts
78
+ // Command with positional arguments
79
+
80
+ export const command = 'create [name]';
81
+ export const describe = 'Create a new migration';
82
+
83
+ export const builder = (args: Argv): Argv<Options> => {
84
+ return args.positional('name', {
85
+ describe: 'Name of the migration',
86
+ type: 'string',
87
+ demandOption: true
88
+ });
89
+ };
90
+
91
+ export const handler = async (args: ArgumentsCamelCase<Options>) => {
92
+ // Implementation
93
+ };
94
+ ```
95
+
96
+ ```ts
97
+ // Must be named $default.ts - Default command (runs when no command is specified)
98
+
99
+ export const describe = 'Default command';
100
+
101
+ export const handler = async (args: ArgumentsCamelCase<Options>) => {
102
+ console.log('Running default command');
103
+ };
104
+ ```
105
+
74
106
  The above will result in these commands being registered:
75
107
 
76
108
  ```
@@ -10,4 +10,6 @@ export interface Command {
10
10
  segments: string[];
11
11
  /** The Yargs command module implementation */
12
12
  commandModule: CommandModule;
13
+ /** Whether this is the default command */
14
+ isDefault?: boolean;
13
15
  }
@@ -38,7 +38,7 @@ function insertIntoTree(treeNodes, command, depth) {
38
38
  });
39
39
  }
40
40
  else if (currentSegment.type === 'internal') {
41
- throw new Error(`Conflict: ${currentSegmentName} is both a directory and a command`);
41
+ throw new Error(`Conflict: ${currentSegmentName} is both a directory and a command ${JSON.stringify(currentSegment)},${JSON.stringify(command)}`);
42
42
  }
43
43
  return;
44
44
  }
@@ -52,7 +52,7 @@ function insertIntoTree(treeNodes, command, depth) {
52
52
  treeNodes.push(currentSegment);
53
53
  }
54
54
  else if (currentSegment.type === 'leaf') {
55
- throw new Error(`Conflict: ${currentSegmentName} is both a directory and a command`);
55
+ throw new Error(`Conflict: ${currentSegmentName} is both a directory and a command ${JSON.stringify(currentSegment)}, ${JSON.stringify(command)}`);
56
56
  }
57
57
  // Recurse into children
58
58
  insertIntoTree(currentSegment.children, command, depth + 1);
@@ -1,43 +1,36 @@
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
- await 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');
1
+ import path from 'path';
2
+ import { test } from 'node:test';
3
+ import assert from 'node:assert';
4
+ import { fileCommands } from './fileCommands.js';
5
+ // get __dirname in ESM style
6
+ const __dirname = path.dirname(new URL(import.meta.url).pathname);
7
+ test('should load commands from directory structure', async () => {
8
+ const commands = await fileCommands({
9
+ commandDirs: [path.join(__dirname, 'fixtures', 'commands')],
10
+ logLevel: 'debug'
29
11
  });
30
- await 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"');
12
+ assert.ok(commands.length > 0);
13
+ });
14
+ test('should respect ignore patterns', async () => {
15
+ const commands = await fileCommands({
16
+ commandDirs: [path.join(__dirname, 'fixtures', 'commands')],
17
+ ignorePatterns: [/health/, /.d.ts/],
18
+ logLevel: 'debug'
19
+ });
20
+ assert.ok(commands.length > 0);
21
+ });
22
+ test('should handle explicit commands and default command', async () => {
23
+ const commands = await fileCommands({
24
+ commandDirs: [path.join(__dirname, 'fixtures', 'commands')],
25
+ logLevel: 'debug'
42
26
  });
27
+ console.log('commands', JSON.stringify(commands.map((c) => c.command), null, 2));
28
+ // Find the explicit command
29
+ const explicitCommand = commands.find((cmd) => cmd.command?.toString().includes('create [name]'));
30
+ assert.ok(explicitCommand, 'Should find explicit command');
31
+ assert.equal(explicitCommand?.describe, 'Create something with a name');
32
+ // Find the default command
33
+ const defaultCommand = commands.find((cmd) => cmd.command === '$0');
34
+ assert.ok(defaultCommand, 'Should find default command');
35
+ assert.equal(defaultCommand?.describe, 'Default command');
43
36
  });
@@ -0,0 +1,2 @@
1
+ export declare const describe = "Default command";
2
+ export declare const handler: () => Promise<void>;
@@ -0,0 +1,4 @@
1
+ export const describe = 'Default command';
2
+ export const handler = async () => {
3
+ // Default command implementation
4
+ };
@@ -0,0 +1,3 @@
1
+ export declare const command = "create [name]";
2
+ export declare const describe = "Create something with a name";
3
+ export declare const handler: () => Promise<void>;
@@ -0,0 +1,5 @@
1
+ export const command = 'create [name]';
2
+ export const describe = 'Create something with a name';
3
+ export const handler = async () => {
4
+ // Create command implementation
5
+ };
@@ -18,8 +18,10 @@ export const importCommandFromFile = async (filePath, name, options) => {
18
18
  }
19
19
  const handlerModule = (await import(filePath));
20
20
  const { logLevel = 'info' } = options;
21
+ // Check if this is the default command
22
+ const isDefault = name === '$default';
21
23
  const command = {
22
- command: name,
24
+ command: handlerModule.command ?? (isDefault ? '$0' : name),
23
25
  describe: handlerModule.describe,
24
26
  alias: handlerModule.alias,
25
27
  builder: handlerModule.builder,
@@ -29,6 +31,19 @@ export const importCommandFromFile = async (filePath, name, options) => {
29
31
  // null implementation
30
32
  })
31
33
  };
34
+ const supportedNames = [
35
+ 'command',
36
+ 'describe',
37
+ 'alias',
38
+ 'builder',
39
+ 'deprecated',
40
+ 'handler'
41
+ ];
42
+ const module = handlerModule;
43
+ const unsupportedExports = Object.keys(module).filter((key) => !supportedNames.includes(key));
44
+ if (unsupportedExports.length > 0) {
45
+ throw new Error(`Command module ${name} in ${filePath} has some unsupported exports, probably a misspelling: ${unsupportedExports.join(', ')}`);
46
+ }
32
47
  if (logLevel === 'debug') {
33
48
  console.debug('Importing command from', filePath, 'as', name, 'with description', command.describe);
34
49
  }
@@ -1,21 +1,42 @@
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
- await 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');
1
+ import path from 'path';
2
+ import { test } from 'node:test';
3
+ import assert from 'node:assert';
4
+ import { importCommandFromFile } from './importCommand.js';
5
+ // get __dirname in ESM style
6
+ const __dirname = path.dirname(new URL(import.meta.url).pathname);
7
+ test('should import command module correctly', async () => {
8
+ const filePath = path.join(__dirname, 'fixtures', 'commands', 'db', 'health.js');
9
+ const command = await importCommandFromFile(filePath, 'health', {
10
+ logLevel: 'info'
14
11
  });
15
- await 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');
12
+ assert.equal(command.describe, 'Database health check');
13
+ });
14
+ test('should handle non-existent files', async () => {
15
+ const filePath = path.join(__dirname, 'fixtures', 'commands', 'non-existent.js');
16
+ try {
17
+ await importCommandFromFile(filePath, 'non-existent', {
18
+ logLevel: 'info'
19
+ });
20
+ assert.fail('Should have thrown an error');
21
+ }
22
+ catch (error) {
23
+ assert.ok(error instanceof Error);
24
+ assert.ok(error.message.includes('Can not import command from non-existence'));
25
+ }
26
+ });
27
+ test('should handle explicit command names', async () => {
28
+ const filePath = path.join(__dirname, 'fixtures', 'commands', 'create.js');
29
+ const command = await importCommandFromFile(filePath, 'create', {
30
+ logLevel: 'info'
31
+ });
32
+ assert.equal(command.command, 'create [name]');
33
+ assert.equal(command.describe, 'Create something with a name');
34
+ });
35
+ test('should handle default commands', async () => {
36
+ const filePath = path.join(__dirname, 'fixtures', 'commands', '$default.js');
37
+ const command = await importCommandFromFile(filePath, '$default', {
38
+ logLevel: 'info'
20
39
  });
40
+ assert.equal(command.command, '$0');
41
+ assert.equal(command.describe, 'Default command');
21
42
  });
@@ -12,7 +12,7 @@ describe('scanDirectory', async () => {
12
12
  extensions: ['.js'],
13
13
  logLevel: 'debug'
14
14
  });
15
- assert.equal(files.length, 2, `Should find two command files, instead found: ${files.join(', ')}`);
15
+ assert.equal(files.length, 4, `Should find two command files, instead found: ${files.join(', ')}`);
16
16
  assert(files.some((f) => f.includes('health.js')), `Should find health.js, instead found: ${files.join(', ')}`);
17
17
  assert(files.some((f) => f.includes('command.js')), `Should find command.js, instead found: ${files.join(', ')}`);
18
18
  });
@@ -21,10 +21,10 @@ describe('scanDirectory', async () => {
21
21
  console.log('Scan Directory: ', commandsDir);
22
22
  const files = await scanDirectory(commandsDir, commandsDir, {
23
23
  extensions: ['.js'],
24
- ignorePatterns: [/health/],
24
+ ignorePatterns: [/health/, /.d.ts/],
25
25
  logLevel: 'debug'
26
26
  });
27
- assert.equal(files.length, 1, `Should find one command file, instead found: ${files.join(', ')}`);
27
+ assert.equal(files.length, 3, `Should find one command file, instead found: ${files.join(', ')}`);
28
28
  assert(files.some((f) => f.includes('command.js')), `Should find command.js, instead found: ${files.join(', ')}`);
29
29
  assert(!files.some((f) => f.includes('health.js')), `Should not find health.js, instead found: ${files.join(', ')}`);
30
30
  });
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.14",
4
+ "version": "0.0.17",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -11,4 +11,6 @@ export interface Command {
11
11
  segments: string[];
12
12
  /** The Yargs command module implementation */
13
13
  commandModule: CommandModule;
14
+ /** Whether this is the default command */
15
+ isDefault?: boolean;
14
16
  }
@@ -75,7 +75,9 @@ function insertIntoTree(
75
75
  });
76
76
  } else if (currentSegment.type === 'internal') {
77
77
  throw new Error(
78
- `Conflict: ${currentSegmentName} is both a directory and a command`
78
+ `Conflict: ${currentSegmentName} is both a directory and a command ${JSON.stringify(
79
+ currentSegment
80
+ )},${JSON.stringify(command)}`
79
81
  );
80
82
  }
81
83
  return;
@@ -91,7 +93,9 @@ function insertIntoTree(
91
93
  treeNodes.push(currentSegment);
92
94
  } else if (currentSegment.type === 'leaf') {
93
95
  throw new Error(
94
- `Conflict: ${currentSegmentName} is both a directory and a command`
96
+ `Conflict: ${currentSegmentName} is both a directory and a command ${JSON.stringify(
97
+ currentSegment
98
+ )}, ${JSON.stringify(command)}`
95
99
  );
96
100
  }
97
101
 
@@ -1,57 +1,55 @@
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
-
6
- import type { CommandModule } from 'yargs';
7
- import yargs from 'yargs';
8
- import { hideBin } from 'yargs/helpers';
9
-
10
- import { fileCommands } from '../lib/fileCommands.js';
11
-
12
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
-
14
- describe('fileCommands', async () => {
15
- await it('should load commands from directory structure', async () => {
16
- const commandsDir = path.join(__dirname, 'fixtures', 'commands');
17
- const commands = await fileCommands({
18
- commandDirs: [commandsDir],
19
- extensions: ['.js'],
20
- logLevel: 'debug'
21
- });
22
-
23
- assert.equal(commands.length, 1, 'Should have one root command');
24
- const rootCommand = commands[0] as CommandModule;
25
- assert(rootCommand, 'Root command should exist');
26
- assert.equal(rootCommand.command, 'db', 'Root command should be "db"');
27
-
28
- // Create a new yargs instance
29
- const yargsInstance = yargs(hideBin(process.argv));
30
-
31
- if (typeof rootCommand.builder === 'function') {
32
- rootCommand.builder(yargsInstance);
33
- }
34
-
35
- // Check that the command has subcommands by checking its description
36
- const description = rootCommand.describe;
37
- assert(
38
- typeof description === 'string' && description.includes('db'),
39
- 'Command should have correct description'
40
- );
1
+ import path from 'path';
2
+ import { test } from 'node:test';
3
+ import assert from 'node:assert';
4
+ import type { Command } from './Command.js';
5
+
6
+ import { fileCommands } from './fileCommands.js';
7
+
8
+ // get __dirname in ESM style
9
+ const __dirname = path.dirname(new URL(import.meta.url).pathname);
10
+
11
+ test('should load commands from directory structure', async () => {
12
+ const commands = await fileCommands({
13
+ commandDirs: [path.join(__dirname, 'fixtures', 'commands')],
14
+ logLevel: 'debug'
15
+ });
16
+
17
+ assert.ok(commands.length > 0);
18
+ });
19
+
20
+ test('should respect ignore patterns', async () => {
21
+ const commands = await fileCommands({
22
+ commandDirs: [path.join(__dirname, 'fixtures', 'commands')],
23
+ ignorePatterns: [/health/, /.d.ts/],
24
+ logLevel: 'debug'
41
25
  });
42
26
 
43
- await it('should respect ignore patterns', async () => {
44
- const commandsDir = path.join(__dirname, 'fixtures', 'commands');
45
- const commands = await fileCommands({
46
- commandDirs: [commandsDir],
47
- extensions: ['.js'],
48
- ignorePatterns: [/health/],
49
- logLevel: 'debug'
50
- });
51
-
52
- assert.equal(commands.length, 1, 'Should have one root command');
53
- const rootCommand = commands[0] as CommandModule;
54
- assert(rootCommand, 'Root command should exist');
55
- assert.equal(rootCommand.command, 'db', 'Root command should be "db"');
27
+ assert.ok(commands.length > 0);
28
+ });
29
+
30
+ test('should handle explicit commands and default command', async () => {
31
+ const commands = await fileCommands({
32
+ commandDirs: [path.join(__dirname, 'fixtures', 'commands')],
33
+ logLevel: 'debug'
56
34
  });
35
+
36
+ console.log(
37
+ 'commands',
38
+ JSON.stringify(
39
+ commands.map((c) => c.command),
40
+ null,
41
+ 2
42
+ )
43
+ );
44
+ // Find the explicit command
45
+ const explicitCommand = commands.find((cmd) =>
46
+ cmd.command?.toString().includes('create [name]')
47
+ );
48
+ assert.ok(explicitCommand, 'Should find explicit command');
49
+ assert.equal(explicitCommand?.describe, 'Create something with a name');
50
+
51
+ // Find the default command
52
+ const defaultCommand = commands.find((cmd) => cmd.command === '$0');
53
+ assert.ok(defaultCommand, 'Should find default command');
54
+ assert.equal(defaultCommand?.describe, 'Default command');
57
55
  });
@@ -0,0 +1,5 @@
1
+ export const describe = 'Default command';
2
+
3
+ export const handler = async () => {
4
+ // Default command implementation
5
+ };
@@ -0,0 +1,6 @@
1
+ export const command = 'create [name]';
2
+ export const describe = 'Create something with a name';
3
+
4
+ export const handler = async () => {
5
+ // Create command implementation
6
+ };
@@ -1,48 +1,65 @@
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
-
6
- import { importCommandFromFile } from '../lib/importCommand.js';
7
-
8
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
-
10
- describe('importCommandFromFile', async () => {
11
- await it('should import command module correctly', async () => {
12
- const commandPath = path.join(
13
- __dirname,
14
- 'fixtures',
15
- 'commands',
16
- 'db',
17
- 'health.js'
18
- );
19
- const command = await importCommandFromFile(commandPath, 'health', {});
1
+ import path from 'path';
2
+ import { test } from 'node:test';
3
+ import assert from 'node:assert';
20
4
 
21
- assert(command.describe, 'Should have describe property');
22
- assert(
23
- typeof command.builder === 'function',
24
- 'Should have builder function'
25
- );
26
- assert(
27
- typeof command.handler === 'function',
28
- 'Should have handler function'
29
- );
5
+ import { importCommandFromFile } from './importCommand.js';
6
+
7
+ // get __dirname in ESM style
8
+ const __dirname = path.dirname(new URL(import.meta.url).pathname);
9
+
10
+ test('should import command module correctly', async () => {
11
+ const filePath = path.join(
12
+ __dirname,
13
+ 'fixtures',
14
+ 'commands',
15
+ 'db',
16
+ 'health.js'
17
+ );
18
+ const command = await importCommandFromFile(filePath, 'health', {
19
+ logLevel: 'info'
30
20
  });
31
21
 
32
- await it('should handle non-existent files', async () => {
33
- const nonExistentPath = path.join(
34
- __dirname,
35
- 'fixtures',
36
- 'commands',
37
- 'non-existent.ts'
38
- );
22
+ assert.equal(command.describe, 'Database health check');
23
+ });
39
24
 
40
- await assert.rejects(
41
- async () => {
42
- await importCommandFromFile(nonExistentPath, 'non-existent', {});
43
- },
44
- Error,
45
- 'Should throw error for non-existent file'
25
+ test('should handle non-existent files', async () => {
26
+ const filePath = path.join(
27
+ __dirname,
28
+ 'fixtures',
29
+ 'commands',
30
+ 'non-existent.js'
31
+ );
32
+ try {
33
+ await importCommandFromFile(filePath, 'non-existent', {
34
+ logLevel: 'info'
35
+ });
36
+ assert.fail('Should have thrown an error');
37
+ } catch (error) {
38
+ assert.ok(error instanceof Error);
39
+ assert.ok(
40
+ (error as Error).message.includes(
41
+ 'Can not import command from non-existence'
42
+ )
46
43
  );
44
+ }
45
+ });
46
+
47
+ test('should handle explicit command names', async () => {
48
+ const filePath = path.join(__dirname, 'fixtures', 'commands', 'create.js');
49
+ const command = await importCommandFromFile(filePath, 'create', {
50
+ logLevel: 'info'
47
51
  });
52
+
53
+ assert.equal(command.command, 'create [name]');
54
+ assert.equal(command.describe, 'Create something with a name');
55
+ });
56
+
57
+ test('should handle default commands', async () => {
58
+ const filePath = path.join(__dirname, 'fixtures', 'commands', '$default.js');
59
+ const command = await importCommandFromFile(filePath, '$default', {
60
+ logLevel: 'info'
61
+ });
62
+
63
+ assert.equal(command.command, '$0');
64
+ assert.equal(command.describe, 'Default command');
48
65
  });
@@ -95,8 +95,11 @@ export const importCommandFromFile = async (
95
95
  const handlerModule = (await import(filePath)) as CommandImportModule;
96
96
  const { logLevel = 'info' } = options;
97
97
 
98
+ // Check if this is the default command
99
+ const isDefault = name === '$default';
100
+
98
101
  const command = {
99
- command: name,
102
+ command: handlerModule.command ?? (isDefault ? '$0' : name),
100
103
  describe: handlerModule.describe,
101
104
  alias: handlerModule.alias,
102
105
  builder: handlerModule.builder,
@@ -108,6 +111,26 @@ export const importCommandFromFile = async (
108
111
  })
109
112
  } as CommandModule;
110
113
 
114
+ const supportedNames = [
115
+ 'command',
116
+ 'describe',
117
+ 'alias',
118
+ 'builder',
119
+ 'deprecated',
120
+ 'handler'
121
+ ];
122
+ const module = handlerModule as Record<string, any>;
123
+ const unsupportedExports = Object.keys(module).filter(
124
+ (key) => !supportedNames.includes(key)
125
+ );
126
+ if (unsupportedExports.length > 0) {
127
+ throw new Error(
128
+ `Command module ${name} in ${filePath} has some unsupported exports, probably a misspelling: ${unsupportedExports.join(
129
+ ', '
130
+ )}`
131
+ );
132
+ }
133
+
111
134
  if (logLevel === 'debug') {
112
135
  console.debug(
113
136
  'Importing command from',
@@ -118,5 +141,6 @@ export const importCommandFromFile = async (
118
141
  command.describe
119
142
  );
120
143
  }
144
+
121
145
  return command;
122
146
  };
@@ -18,7 +18,7 @@ describe('scanDirectory', async () => {
18
18
 
19
19
  assert.equal(
20
20
  files.length,
21
- 2,
21
+ 4,
22
22
  `Should find two command files, instead found: ${files.join(', ')}`
23
23
  );
24
24
  assert(
@@ -36,13 +36,13 @@ describe('scanDirectory', async () => {
36
36
  console.log('Scan Directory: ', commandsDir);
37
37
  const files = await scanDirectory(commandsDir, commandsDir, {
38
38
  extensions: ['.js'],
39
- ignorePatterns: [/health/],
39
+ ignorePatterns: [/health/, /.d.ts/],
40
40
  logLevel: 'debug'
41
41
  });
42
42
 
43
43
  assert.equal(
44
44
  files.length,
45
- 1,
45
+ 3,
46
46
  `Should find one command file, instead found: ${files.join(', ')}`
47
47
  );
48
48
  assert(