yargs-file-commands 0.0.20 → 1.1.0

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.
Files changed (61) hide show
  1. package/README.md +73 -109
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +2 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/lib/Command.js +1 -0
  6. package/dist/lib/Command.js.map +1 -0
  7. package/dist/lib/buildSegmentTree.d.ts +1 -1
  8. package/dist/lib/buildSegmentTree.js +7 -3
  9. package/dist/lib/buildSegmentTree.js.map +1 -0
  10. package/dist/lib/buildSegmentTree.test.js +279 -26
  11. package/dist/lib/buildSegmentTree.test.js.map +1 -0
  12. package/dist/lib/defineCommand.d.ts +37 -0
  13. package/dist/lib/defineCommand.js +5 -0
  14. package/dist/lib/defineCommand.js.map +1 -0
  15. package/dist/lib/fileCommands.d.ts +2 -1
  16. package/dist/lib/fileCommands.js +39 -21
  17. package/dist/lib/fileCommands.js.map +1 -0
  18. package/dist/lib/fileCommands.test.js +107 -30
  19. package/dist/lib/fileCommands.test.js.map +1 -0
  20. package/dist/lib/fixtures/commands/$default.js +1 -0
  21. package/dist/lib/fixtures/commands/$default.js.map +1 -0
  22. package/dist/lib/fixtures/commands/create.js +1 -0
  23. package/dist/lib/fixtures/commands/create.js.map +1 -0
  24. package/dist/lib/fixtures/commands/db/health.d.ts +2 -1
  25. package/dist/lib/fixtures/commands/db/health.js +2 -3
  26. package/dist/lib/fixtures/commands/db/health.js.map +1 -0
  27. package/dist/lib/fixtures/commands/db/migration/command.d.ts +9 -2
  28. package/dist/lib/fixtures/commands/db/migration/command.js +6 -7
  29. package/dist/lib/fixtures/commands/db/migration/command.js.map +1 -0
  30. package/dist/lib/importCommand.d.ts +3 -3
  31. package/dist/lib/importCommand.js +39 -27
  32. package/dist/lib/importCommand.js.map +1 -0
  33. package/dist/lib/importCommand.test.js +157 -33
  34. package/dist/lib/importCommand.test.js.map +1 -0
  35. package/dist/lib/scanDirectory.js +54 -25
  36. package/dist/lib/scanDirectory.js.map +1 -0
  37. package/dist/lib/scanDirectory.test.js +148 -25
  38. package/dist/lib/scanDirectory.test.js.map +1 -0
  39. package/dist/lib/segmentPath.js +8 -6
  40. package/dist/lib/segmentPath.js.map +1 -0
  41. package/dist/lib/segmentPath.test.js +10 -38
  42. package/dist/lib/segmentPath.test.js.map +1 -0
  43. package/dist/tsconfig.tsbuildinfo +1 -0
  44. package/package.json +6 -9
  45. package/CHANGELOG.md +0 -62
  46. package/src/index.ts +0 -1
  47. package/src/lib/Command.ts +0 -16
  48. package/src/lib/buildSegmentTree.test.ts +0 -90
  49. package/src/lib/buildSegmentTree.ts +0 -149
  50. package/src/lib/fileCommands.test.ts +0 -55
  51. package/src/lib/fileCommands.ts +0 -149
  52. package/src/lib/fixtures/commands/$default.ts +0 -5
  53. package/src/lib/fixtures/commands/create.ts +0 -6
  54. package/src/lib/fixtures/commands/db/health.ts +0 -9
  55. package/src/lib/fixtures/commands/db/migration/command.ts +0 -12
  56. package/src/lib/importCommand.test.ts +0 -60
  57. package/src/lib/importCommand.ts +0 -196
  58. package/src/lib/scanDirectory.test.ts +0 -75
  59. package/src/lib/scanDirectory.ts +0 -109
  60. package/src/lib/segmentPath.test.ts +0 -71
  61. package/src/lib/segmentPath.ts +0 -38
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  [![NPM Package][npm]][npm-url]
4
4
  [![NPM Downloads][npm-downloads]][npmtrends-url]
5
+ [![Tests][tests-badge]][tests-url]
6
+ [![Coverage][coverage-badge]][coverage-url]
5
7
 
6
8
  This Yargs helper function lets you define all your commands as individual files and their file names and directory structure defines via implication your nested command structure.
7
9
 
@@ -17,20 +19,30 @@ npm install yargs-file-commands
17
19
 
18
20
  ## Example
19
21
 
22
+ ### 1. Setup
23
+
24
+ First, configure your entry point to scan your commands directory:
25
+
20
26
  ```ts
27
+ import path from 'path';
28
+ import yargs from 'yargs';
29
+ import { hideBin } from 'yargs/helpers';
30
+ import { fileCommands } from 'yargs-file-commands';
31
+
21
32
  export const main = async () => {
22
- const commandsDir = path.join(distDir, 'commands');
33
+ const commandsDir = path.join(process.cwd(), 'dist/commands');
23
34
 
24
35
  return yargs(hideBin(process.argv))
25
- .scriptName(packageInfo.name!)
26
- .version(packageInfo.version!)
36
+ .scriptName('my-cli')
27
37
  .command(
28
- await fileCommands({ commandDirs: [commandsDir], logLevel: 'debug' })
38
+ await fileCommands({ commandDirs: [commandsDir] })
29
39
  )
30
40
  .help().argv;
31
41
  };
32
42
  ```
33
43
 
44
+ ### 2. File Structure
45
+
34
46
  You can use any combination of file names and directories. We support either [NextJS](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes) or [Remix](https://remix.run/docs/en/main/file-conventions/routes) conventions for interpreting filenames and directories.
35
47
 
36
48
  ```
@@ -43,123 +55,77 @@ You can use any combination of file names and directories. We support either [Ne
43
55
  └── studio.start.ts // the "studio start" command
44
56
  ```
45
57
 
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:
58
+ The above will result in these commands being registered:
47
59
 
48
- ```ts
49
- // commands/studio.start.ts - Basic command using filename as command name
60
+ ```
61
+ db migration
62
+ db health
63
+ studio start
64
+ ```
50
65
 
51
- import type { ArgumentsCamelCase, Argv } from 'yargs';
52
- import type { BaseOptions } from '../options.js';
66
+ ### 3. Define Commands
53
67
 
54
- export interface Options extends BaseOptions {
55
- port?: number;
56
- }
68
+ Use the `defineCommand` helper to define your commands. This ensures full type safety for your arguments based on the options you define in the `builder`.
57
69
 
58
- export const command = 'start'; // this is optional, it will use the filename if this isn't specified
70
+ **Basic Command (`commands/studio.start.ts`)**
59
71
 
60
- export const describe = 'Studio web interface';
72
+ ```ts
73
+ import { defineCommand } from 'yargs-file-commands';
61
74
 
62
- export const builder = (args: Argv): Argv<Options> => {
63
- const result = args.option('port', {
75
+ export const command = defineCommand({
76
+ command: 'start', // Optional: defaults to filename if omitted
77
+ describe: 'Studio web interface',
78
+ builder: (yargs) => yargs.option('port', {
64
79
  alias: 'p',
65
80
  type: 'number',
66
- describe: 'Port to listen on'
67
- });
68
- return result;
69
- };
70
-
71
- export const handler = async (args: ArgumentsCamelCase<Options>) => {
72
- const config = await getConfig();
73
- // Implementation
74
- };
81
+ describe: 'Port to listen on',
82
+ default: 3000
83
+ }),
84
+ handler: async (argv) => {
85
+ // argv.port is correctly typed as number
86
+ console.log(`Starting studio on port ${argv.port}`);
87
+ }
88
+ });
75
89
  ```
76
90
 
77
- ```ts
78
- // Command with positional arguments
91
+ **Positional Arguments (`commands/create.ts`)**
79
92
 
80
- export const command = 'create [name]';
81
- export const describe = 'Create a new migration';
93
+ ```ts
94
+ import { defineCommand } from 'yargs-file-commands';
82
95
 
83
- export const builder = (args: Argv): Argv<Options> => {
84
- return args.positional('name', {
85
- describe: 'Name of the migration',
96
+ export const command = defineCommand({
97
+ command: 'create <name>', // Define positional args in the command string
98
+ describe: 'Create a new resource',
99
+ builder: (yargs) => yargs.positional('name', {
100
+ describe: 'Name of the resource',
86
101
  type: 'string',
87
102
  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
-
106
- The above will result in these commands being registered:
107
-
108
- ```
109
- db migration
110
- db health
111
- studio start
103
+ }),
104
+ handler: async (argv) => {
105
+ // argv.name is correctly typed as string
106
+ console.log(`Creating resource: ${argv.name}`);
107
+ }
108
+ });
112
109
  ```
113
110
 
114
- ### Alternative Type-Safe Command Definition
111
+ **Default Command (`commands/$default.ts`)**
115
112
 
116
- YOu can also use this type-safe way to define commands using the `CommandModule` type from yargs directly. This is the preferred method as it provides better TypeScript support and catches potential errors at compile time rather than runtime:
113
+ This command runs when no other command is specified.
117
114
 
118
115
  ```ts
119
- import type { ArgumentsCamelCase, CommandModule } from 'yargs';
120
-
121
- type TriageArgs = {
122
- owner: string;
123
- repo: string;
124
- issue: number;
125
- };
116
+ import { defineCommand } from 'yargs-file-commands';
126
117
 
127
- export const command: CommandModule<object, TriageArgs> = {
128
- command: 'triage <owner> <repo> <issue>',
129
- describe: 'Triage a GitHub issue',
130
- builder: {
131
- owner: {
132
- type: 'string',
133
- description: 'GitHub repository owner',
134
- demandOption: true
135
- },
136
- repo: {
137
- type: 'string',
138
- description: 'GitHub repository name',
139
- demandOption: true
140
- },
141
- issue: {
142
- type: 'number',
143
- description: 'Issue number',
144
- demandOption: true
145
- }
146
- },
147
- handler: async (argv: ArgumentsCamelCase<TriageArgs>) => {
148
- // Implementation
118
+ export const command = defineCommand({
119
+ describe: 'Default command',
120
+ handler: async (argv) => {
121
+ console.log('Running default command');
149
122
  }
150
- };
123
+ });
151
124
  ```
152
125
 
153
- This approach has several advantages:
154
-
155
- - Full TypeScript support with proper type inference
156
- - Compile-time checking of command structure
157
- - No risk of misspelling exports
158
- - Better IDE support with autocompletion
159
-
160
126
  ## Options
161
127
 
162
- The "fileCommands" method takes the following options:
128
+ The `fileCommands` method takes the following options:
163
129
 
164
130
  **commandDirs**
165
131
 
@@ -187,25 +153,19 @@ If you want to contribute, just check out [this git project](https://github.com/
187
153
 
188
154
  ```sh
189
155
  # install dependencies
190
- npm install
156
+ pnpm install
191
157
 
192
158
  # build everything
193
- npm run build
159
+ pnpm run build
194
160
 
195
- # prettify
196
- npm run format
161
+ # biome
162
+ pnpm run chec
197
163
 
198
- # eslint
199
- npm run lint
200
-
201
- # build and run tests
202
- npm run test
164
+ # tests
165
+ pnpm vitest
203
166
 
204
167
  # clean everything, should be like doing a fresh git checkout of the repo.
205
- npm run clean
206
-
207
- # publish the npm package
208
- npm run publish
168
+ pnpm clean
209
169
 
210
170
  # run example cli
211
171
  npx example-cli
@@ -217,3 +177,7 @@ Underneath the hood, we are using [NX](https://nx.dev) to manage the monorepo an
217
177
  [npm-url]: https://www.npmjs.com/package/yargs-file-commands
218
178
  [npm-downloads]: https://img.shields.io/npm/dw/yargs-file-commands
219
179
  [npmtrends-url]: https://www.npmtrends.com/yargs-file-commands
180
+ [tests-badge]: https://github.com/bhouston/yargs-file-commands/workflows/Tests/badge.svg
181
+ [tests-url]: https://github.com/bhouston/yargs-file-commands/actions/workflows/test.yml
182
+ [coverage-badge]: https://codecov.io/gh/bhouston/yargs-file-commands/branch/main/graph/badge.svg
183
+ [coverage-url]: https://codecov.io/gh/bhouston/yargs-file-commands
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
1
  export * from './lib/fileCommands.js';
2
+ export * from './lib/defineCommand.js';
package/dist/index.js CHANGED
@@ -1 +1,3 @@
1
1
  export * from './lib/fileCommands.js';
2
+ export * from './lib/defineCommand.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC"}
@@ -1 +1,2 @@
1
1
  export {};
2
+ //# sourceMappingURL=Command.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Command.js","sourceRoot":"","sources":["../../src/lib/Command.ts"],"names":[],"mappings":""}
@@ -38,6 +38,6 @@ export declare const buildSegmentTree: (commands: Command[]) => CommandTreeNode[
38
38
  * For leaf nodes, returns the actual command implementation.
39
39
  * For internal nodes, creates a parent command that manages subcommands.
40
40
  */
41
- export declare const createCommand: (treeNode: CommandTreeNode) => CommandModule;
41
+ export declare const createCommand: (treeNode: CommandTreeNode) => CommandModule<{}, {}>;
42
42
  export declare const logCommandTree: (commands: CommandTreeNode[], level?: number) => void;
43
43
  export {};
@@ -27,6 +27,9 @@ function insertIntoTree(treeNodes, command, depth) {
27
27
  return;
28
28
  }
29
29
  const currentSegmentName = command.segments[depth];
30
+ if (currentSegmentName === undefined) {
31
+ return;
32
+ }
30
33
  let currentSegment = treeNodes.find((s) => s.segmentName === currentSegmentName);
31
34
  // If this is the last segment, create a leaf node
32
35
  if (depth === command.segments.length - 1) {
@@ -34,7 +37,7 @@ function insertIntoTree(treeNodes, command, depth) {
34
37
  treeNodes.push({
35
38
  type: 'leaf',
36
39
  segmentName: currentSegmentName,
37
- command
40
+ command,
38
41
  });
39
42
  }
40
43
  else if (currentSegment.type === 'internal') {
@@ -47,7 +50,7 @@ function insertIntoTree(treeNodes, command, depth) {
47
50
  currentSegment = {
48
51
  type: 'internal',
49
52
  segmentName: currentSegmentName,
50
- children: []
53
+ children: [],
51
54
  };
52
55
  treeNodes.push(currentSegment);
53
56
  }
@@ -85,7 +88,7 @@ export const createCommand = (treeNode) => {
85
88
  },
86
89
  handler: async () => {
87
90
  // Internal nodes don't need handlers as they'll demand subcommands
88
- }
91
+ },
89
92
  };
90
93
  return command;
91
94
  };
@@ -97,3 +100,4 @@ export const logCommandTree = (commands, level = 0) => {
97
100
  }
98
101
  });
99
102
  };
103
+ //# sourceMappingURL=buildSegmentTree.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildSegmentTree.js","sourceRoot":"","sources":["../../src/lib/buildSegmentTree.ts"],"names":[],"mappings":"AA0BA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,QAAmB,EAAqB,EAAE,CAAC;IAC1E,MAAM,aAAa,GAAsB,EAAE,CAAC;IAE5C,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,cAAc,CAAC,aAAa,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;IAC5C,CAAC;IAED,OAAO,aAAa,CAAC;AAAA,CACtB,CAAC;AAEF;;;;;;GAMG;AACH,SAAS,cAAc,CAAC,SAA4B,EAAE,OAAgB,EAAE,KAAa,EAAQ;IAC3F,wDAAwD;IACxD,IAAI,KAAK,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;QACrC,OAAO;IACT,CAAC;IAED,MAAM,kBAAkB,GAAG,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACnD,IAAI,kBAAkB,KAAK,SAAS,EAAE,CAAC;QACrC,OAAO;IACT,CAAC;IACD,IAAI,cAAc,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,KAAK,kBAAkB,CAAC,CAAC;IAEjF,kDAAkD;IAClD,IAAI,KAAK,KAAK,OAAO,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1C,IAAI,cAAc,IAAI,IAAI,EAAE,CAAC;YAC3B,SAAS,CAAC,IAAI,CAAC;gBACb,IAAI,EAAE,MAAM;gBACZ,WAAW,EAAE,kBAAkB;gBAC/B,OAAO;aACR,CAAC,CAAC;QACL,CAAC;aAAM,IAAI,cAAc,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YAC9C,MAAM,IAAI,KAAK,CACb,aAAa,kBAAkB,sCAAsC,IAAI,CAAC,SAAS,CACjF,cAAc,CACf,IAAI,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAC/B,CAAC;QACJ,CAAC;QACD,OAAO;IACT,CAAC;IAED,gDAAgD;IAChD,IAAI,cAAc,IAAI,IAAI,EAAE,CAAC;QAC3B,cAAc,GAAG;YACf,IAAI,EAAE,UAAU;YAChB,WAAW,EAAE,kBAAkB;YAC/B,QAAQ,EAAE,EAAE;SACb,CAAC;QACF,SAAS,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IACjC,CAAC;SAAM,IAAI,cAAc,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CACb,aAAa,kBAAkB,sCAAsC,IAAI,CAAC,SAAS,CACjF,cAAc,CACf,KAAK,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAChC,CAAC;IACJ,CAAC;IAED,wBAAwB;IACxB,cAAc,CAAC,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;AAAA,CAC7D;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,QAAyB,EAAiB,EAAE,CAAC;IACzE,IAAI,QAAQ,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAC7B,OAAO,QAAQ,CAAC,OAAO,CAAC,aAAa,CAAC;IACxC,CAAC;IAED,MAAM,IAAI,GAAG,QAAQ,CAAC,WAAW,CAAC;IAClC,mEAAmE;IACnE,MAAM,OAAO,GAAkB;QAC7B,OAAO,EAAE,IAAI;QACb,QAAQ,EAAE,GAAG,IAAI,WAAW;QAC5B,OAAO,EAAE,CAAC,KAAW,EAAQ,EAAE,CAAC;YAC9B,6CAA6C;YAC7C,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YACtE,+CAA+C;YAC/C,KAAK,CAAC,aAAa,CAAC,CAAC,EAAE,sBAAsB,IAAI,aAAa,CAAC,CAAC;YAEhE,OAAO,KAAK,CAAC;QAAA,CACd;QACD,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC;YACnB,mEAAmE;QAD/C,CAErB;KACF,CAAC;IAEF,OAAO,OAAO,CAAC;AAAA,CAChB,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,QAA2B,EAAE,KAAK,GAAG,CAAC,EAAE,EAAE,CAAC;IACxE,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;QAC5B,OAAO,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;QAC7D,IAAI,OAAO,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YAChC,cAAc,CAAC,OAAO,CAAC,QAAQ,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;QAC9C,CAAC;IAAA,CACF,CAAC,CAAC;AAAA,CACJ,CAAC"}
@@ -1,7 +1,6 @@
1
- import assert from 'node:assert/strict';
2
- import { describe, it } from 'node:test';
3
- import { buildSegmentTree } from './buildSegmentTree.js';
4
- describe('buildSegmentTree', async () => {
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { buildSegmentTree, createCommand, logCommandTree } from './buildSegmentTree.js';
3
+ describe('buildSegmentTree', () => {
5
4
  it('should build correct tree structure', () => {
6
5
  const commands = [
7
6
  {
@@ -12,8 +11,8 @@ describe('buildSegmentTree', async () => {
12
11
  describe: 'Migration command',
13
12
  handler: async () => {
14
13
  // Test handler
15
- }
16
- }
14
+ },
15
+ },
17
16
  },
18
17
  {
19
18
  fullPath: '/commands/db/health.js',
@@ -23,28 +22,29 @@ describe('buildSegmentTree', async () => {
23
22
  describe: 'Health command',
24
23
  handler: async () => {
25
24
  // Test handler
26
- }
27
- }
28
- }
25
+ },
26
+ },
27
+ },
29
28
  ];
30
29
  const tree = buildSegmentTree(commands);
31
- assert.equal(tree.length, 1, 'Should have one root node');
30
+ expect(tree.length).toBe(1);
32
31
  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');
32
+ expect(rootNode).toBeDefined();
33
+ if (!rootNode) {
34
+ throw new Error('Root node should exist');
35
+ }
36
+ expect(rootNode.segmentName).toBe('db');
37
+ expect(rootNode.type).toBe('internal');
36
38
  if (rootNode.type !== 'internal') {
37
39
  throw new Error('Expected internal node');
38
40
  }
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');
41
+ expect(rootNode.children.length).toBe(2);
42
+ const childSegments = rootNode.children.map((child) => child.segmentName).sort();
43
+ expect(childSegments).toEqual(['health', 'migration']);
44
44
  });
45
45
  it('should handle empty input', () => {
46
46
  const tree = buildSegmentTree([]);
47
- assert.equal(tree.length, 0, 'Should return empty array for empty input');
47
+ expect(tree.length).toBe(0);
48
48
  });
49
49
  it('should handle single command', () => {
50
50
  const commands = [
@@ -56,15 +56,268 @@ describe('buildSegmentTree', async () => {
56
56
  describe: 'Test command',
57
57
  handler: async () => {
58
58
  // Test handler
59
- }
60
- }
61
- }
59
+ },
60
+ },
61
+ },
62
62
  ];
63
63
  const tree = buildSegmentTree(commands);
64
- assert.equal(tree.length, 1, 'Should have one node');
64
+ expect(tree.length).toBe(1);
65
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');
66
+ expect(node).toBeDefined();
67
+ if (!node) {
68
+ throw new Error('Node should exist');
69
+ }
70
+ expect(node.segmentName).toBe('test');
71
+ expect(node.type).toBe('leaf');
72
+ });
73
+ it('should throw error when directory conflicts with command name (directory first)', () => {
74
+ const commands = [
75
+ {
76
+ fullPath: '/commands/db/migration/command.js',
77
+ segments: ['db', 'migration'],
78
+ commandModule: {
79
+ command: 'migration',
80
+ describe: 'Migration command',
81
+ handler: async () => { },
82
+ },
83
+ },
84
+ {
85
+ fullPath: '/commands/db.js',
86
+ segments: ['db'],
87
+ commandModule: {
88
+ command: 'db',
89
+ describe: 'DB command',
90
+ handler: async () => { },
91
+ },
92
+ },
93
+ ];
94
+ expect(() => buildSegmentTree(commands)).toThrow(/Conflict: db is both a directory and a command/);
95
+ });
96
+ it('should throw error when directory conflicts with command name (command first)', () => {
97
+ const commands = [
98
+ {
99
+ fullPath: '/commands/db.js',
100
+ segments: ['db'],
101
+ commandModule: {
102
+ command: 'db',
103
+ describe: 'DB command',
104
+ handler: async () => { },
105
+ },
106
+ },
107
+ {
108
+ fullPath: '/commands/db/migration/command.js',
109
+ segments: ['db', 'migration'],
110
+ commandModule: {
111
+ command: 'migration',
112
+ describe: 'Migration command',
113
+ handler: async () => { },
114
+ },
115
+ },
116
+ ];
117
+ expect(() => buildSegmentTree(commands)).toThrow(/Conflict: db is both a directory and a command/);
118
+ });
119
+ it('should handle multiple root commands', () => {
120
+ const commands = [
121
+ {
122
+ fullPath: '/commands/hello.ts',
123
+ segments: ['hello'],
124
+ commandModule: {
125
+ command: 'hello',
126
+ describe: 'Hello command',
127
+ handler: async () => { },
128
+ },
129
+ },
130
+ {
131
+ fullPath: '/commands/world.ts',
132
+ segments: ['world'],
133
+ commandModule: {
134
+ command: 'world',
135
+ describe: 'World command',
136
+ handler: async () => { },
137
+ },
138
+ },
139
+ ];
140
+ const tree = buildSegmentTree(commands);
141
+ expect(tree.length).toBe(2);
142
+ expect(tree.map((n) => n.segmentName).sort()).toEqual(['hello', 'world']);
143
+ });
144
+ it('should handle nested commands at different depths', () => {
145
+ const commands = [
146
+ {
147
+ fullPath: '/commands/a/b/c.ts',
148
+ segments: ['a', 'b', 'c'],
149
+ commandModule: {
150
+ command: 'c',
151
+ describe: 'C command',
152
+ handler: async () => { },
153
+ },
154
+ },
155
+ {
156
+ fullPath: '/commands/a/d.ts',
157
+ segments: ['a', 'd'],
158
+ commandModule: {
159
+ command: 'd',
160
+ describe: 'D command',
161
+ handler: async () => { },
162
+ },
163
+ },
164
+ ];
165
+ const tree = buildSegmentTree(commands);
166
+ expect(tree.length).toBe(1);
167
+ expect(tree[0]?.segmentName).toBe('a');
168
+ if (tree[0]?.type === 'internal') {
169
+ expect(tree[0].children.length).toBe(2);
170
+ const childNames = tree[0].children.map((c) => c.segmentName).sort();
171
+ expect(childNames).toEqual(['b', 'd']);
172
+ const bNode = tree[0].children.find((c) => c.segmentName === 'b');
173
+ if (bNode?.type === 'internal') {
174
+ expect(bNode.children.length).toBe(1);
175
+ expect(bNode.children[0]?.segmentName).toBe('c');
176
+ expect(bNode.children[0]?.type).toBe('leaf');
177
+ }
178
+ }
179
+ });
180
+ });
181
+ describe('createCommand', () => {
182
+ it('should create command module from leaf node', () => {
183
+ const command = {
184
+ fullPath: '/commands/test.ts',
185
+ segments: ['test'],
186
+ commandModule: {
187
+ command: 'test',
188
+ describe: 'Test command',
189
+ handler: async () => {
190
+ // Test handler
191
+ },
192
+ },
193
+ };
194
+ const treeNode = {
195
+ type: 'leaf',
196
+ segmentName: 'test',
197
+ command,
198
+ };
199
+ const commandModule = createCommand(treeNode);
200
+ expect(commandModule.command).toBe('test');
201
+ expect(commandModule.describe).toBe('Test command');
202
+ expect(commandModule.handler).toBe(command.commandModule.handler);
203
+ });
204
+ it('should create command module from internal node with children', () => {
205
+ const childCommand = {
206
+ fullPath: '/commands/db/health.ts',
207
+ segments: ['db', 'health'],
208
+ commandModule: {
209
+ command: 'health',
210
+ describe: 'Health check',
211
+ handler: async () => { },
212
+ },
213
+ };
214
+ const childTreeNode = {
215
+ type: 'leaf',
216
+ segmentName: 'health',
217
+ command: childCommand,
218
+ };
219
+ const internalTreeNode = {
220
+ type: 'internal',
221
+ segmentName: 'db',
222
+ children: [childTreeNode],
223
+ };
224
+ const commandModule = createCommand(internalTreeNode);
225
+ expect(commandModule.command).toBe('db');
226
+ expect(commandModule.describe).toBe('db commands');
227
+ expect(commandModule.builder).toBeDefined();
228
+ expect(commandModule.handler).toBeDefined();
229
+ });
230
+ it('should create nested command structure with builder', () => {
231
+ const healthCommand = {
232
+ fullPath: '/commands/db/health.ts',
233
+ segments: ['db', 'health'],
234
+ commandModule: {
235
+ command: 'health',
236
+ describe: 'Health check',
237
+ handler: async () => { },
238
+ },
239
+ };
240
+ const migrationCommand = {
241
+ fullPath: '/commands/db/migration.ts',
242
+ segments: ['db', 'migration'],
243
+ commandModule: {
244
+ command: 'migration',
245
+ describe: 'Migration',
246
+ handler: async () => { },
247
+ },
248
+ };
249
+ const tree = buildSegmentTree([healthCommand, migrationCommand]);
250
+ const dbNode = tree[0];
251
+ if (!dbNode || dbNode.type !== 'internal') {
252
+ throw new Error('Expected internal node');
253
+ }
254
+ const commandModule = createCommand(dbNode);
255
+ expect(commandModule.command).toBe('db');
256
+ expect(commandModule.builder).toBeDefined();
257
+ // Test the builder function
258
+ if (commandModule.builder && typeof commandModule.builder === 'function') {
259
+ const mockYargs = {
260
+ command: vi.fn().mockReturnThis(),
261
+ demandCommand: vi.fn().mockReturnThis(),
262
+ };
263
+ commandModule.builder(mockYargs);
264
+ expect(mockYargs.command).toHaveBeenCalledTimes(1);
265
+ expect(mockYargs.demandCommand).toHaveBeenCalledWith(1, 'You must specify a db subcommand');
266
+ }
267
+ });
268
+ });
269
+ describe('logCommandTree', () => {
270
+ it('should log command tree structure', () => {
271
+ const consoleSpy = vi.spyOn(console, 'debug').mockImplementation(() => { });
272
+ const commands = [
273
+ {
274
+ fullPath: '/commands/db/health.ts',
275
+ segments: ['db', 'health'],
276
+ commandModule: {
277
+ command: 'health',
278
+ describe: 'Health',
279
+ handler: async () => { },
280
+ },
281
+ },
282
+ {
283
+ fullPath: '/commands/hello.ts',
284
+ segments: ['hello'],
285
+ commandModule: {
286
+ command: 'hello',
287
+ describe: 'Hello',
288
+ handler: async () => { },
289
+ },
290
+ },
291
+ ];
292
+ const tree = buildSegmentTree(commands);
293
+ logCommandTree(tree);
294
+ expect(consoleSpy).toHaveBeenCalled();
295
+ const calls = consoleSpy.mock.calls.map((call) => call[0]);
296
+ expect(calls.some((call) => call.includes('db'))).toBe(true);
297
+ expect(calls.some((call) => call.includes('health'))).toBe(true);
298
+ expect(calls.some((call) => call.includes('hello'))).toBe(true);
299
+ consoleSpy.mockRestore();
300
+ });
301
+ it('should log command tree with correct indentation levels', () => {
302
+ const consoleSpy = vi.spyOn(console, 'debug').mockImplementation(() => { });
303
+ const commands = [
304
+ {
305
+ fullPath: '/commands/a/b/c.ts',
306
+ segments: ['a', 'b', 'c'],
307
+ commandModule: {
308
+ command: 'c',
309
+ describe: 'C',
310
+ handler: async () => { },
311
+ },
312
+ },
313
+ ];
314
+ const tree = buildSegmentTree(commands);
315
+ logCommandTree(tree, 2);
316
+ expect(consoleSpy).toHaveBeenCalled();
317
+ const calls = consoleSpy.mock.calls.map((call) => call[0]);
318
+ // Check for indentation (initial level 2 = 4 spaces, then children add more)
319
+ expect(calls.length).toBeGreaterThan(0);
320
+ consoleSpy.mockRestore();
69
321
  });
70
322
  });
323
+ //# sourceMappingURL=buildSegmentTree.test.js.map