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 +23 -0
- package/README.md +34 -2
- package/dist/lib/Command.d.ts +2 -0
- package/dist/lib/buildSegmentTree.js +2 -2
- package/dist/lib/fileCommands.test.js +33 -40
- package/dist/lib/fixtures/commands/$default.d.ts +2 -0
- package/dist/lib/fixtures/commands/$default.js +4 -0
- package/dist/lib/fixtures/commands/create.d.ts +3 -0
- package/dist/lib/fixtures/commands/create.js +5 -0
- package/dist/lib/importCommand.js +16 -1
- package/dist/lib/importCommand.test.js +39 -18
- package/dist/lib/scanDirectory.test.js +3 -3
- package/package.json +1 -1
- package/src/lib/Command.ts +2 -0
- package/src/lib/buildSegmentTree.ts +6 -2
- package/src/lib/fileCommands.test.ts +51 -53
- package/src/lib/fixtures/commands/$default.ts +5 -0
- package/src/lib/fixtures/commands/create.ts +6 -0
- package/src/lib/importCommand.test.ts +58 -41
- package/src/lib/importCommand.ts +25 -1
- package/src/lib/scanDirectory.test.ts +3 -3
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
|
|
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
|
```
|
package/dist/lib/Command.d.ts
CHANGED
|
@@ -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
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
});
|
|
@@ -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
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
const __dirname = path.dirname(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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,
|
|
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,
|
|
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.
|
|
4
|
+
"version": "0.0.17",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
package/src/lib/Command.ts
CHANGED
|
@@ -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
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
});
|
|
@@ -1,48 +1,65 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
__dirname,
|
|
35
|
-
'fixtures',
|
|
36
|
-
'commands',
|
|
37
|
-
'non-existent.ts'
|
|
38
|
-
);
|
|
22
|
+
assert.equal(command.describe, 'Database health check');
|
|
23
|
+
});
|
|
39
24
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
});
|
package/src/lib/importCommand.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
45
|
+
3,
|
|
46
46
|
`Should find one command file, instead found: ${files.join(', ')}`
|
|
47
47
|
);
|
|
48
48
|
assert(
|