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.
- package/README.md +73 -109
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/Command.js +1 -0
- package/dist/lib/Command.js.map +1 -0
- package/dist/lib/buildSegmentTree.d.ts +1 -1
- package/dist/lib/buildSegmentTree.js +7 -3
- package/dist/lib/buildSegmentTree.js.map +1 -0
- package/dist/lib/buildSegmentTree.test.js +279 -26
- package/dist/lib/buildSegmentTree.test.js.map +1 -0
- package/dist/lib/defineCommand.d.ts +37 -0
- package/dist/lib/defineCommand.js +5 -0
- package/dist/lib/defineCommand.js.map +1 -0
- package/dist/lib/fileCommands.d.ts +2 -1
- package/dist/lib/fileCommands.js +39 -21
- package/dist/lib/fileCommands.js.map +1 -0
- package/dist/lib/fileCommands.test.js +107 -30
- package/dist/lib/fileCommands.test.js.map +1 -0
- package/dist/lib/fixtures/commands/$default.js +1 -0
- package/dist/lib/fixtures/commands/$default.js.map +1 -0
- package/dist/lib/fixtures/commands/create.js +1 -0
- package/dist/lib/fixtures/commands/create.js.map +1 -0
- package/dist/lib/fixtures/commands/db/health.d.ts +2 -1
- package/dist/lib/fixtures/commands/db/health.js +2 -3
- package/dist/lib/fixtures/commands/db/health.js.map +1 -0
- package/dist/lib/fixtures/commands/db/migration/command.d.ts +9 -2
- package/dist/lib/fixtures/commands/db/migration/command.js +6 -7
- package/dist/lib/fixtures/commands/db/migration/command.js.map +1 -0
- package/dist/lib/importCommand.d.ts +3 -3
- package/dist/lib/importCommand.js +39 -27
- package/dist/lib/importCommand.js.map +1 -0
- package/dist/lib/importCommand.test.js +157 -33
- package/dist/lib/importCommand.test.js.map +1 -0
- package/dist/lib/scanDirectory.js +54 -25
- package/dist/lib/scanDirectory.js.map +1 -0
- package/dist/lib/scanDirectory.test.js +148 -25
- package/dist/lib/scanDirectory.test.js.map +1 -0
- package/dist/lib/segmentPath.js +8 -6
- package/dist/lib/segmentPath.js.map +1 -0
- package/dist/lib/segmentPath.test.js +10 -38
- package/dist/lib/segmentPath.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +6 -9
- package/CHANGELOG.md +0 -62
- package/src/index.ts +0 -1
- package/src/lib/Command.ts +0 -16
- package/src/lib/buildSegmentTree.test.ts +0 -90
- package/src/lib/buildSegmentTree.ts +0 -149
- package/src/lib/fileCommands.test.ts +0 -55
- package/src/lib/fileCommands.ts +0 -149
- package/src/lib/fixtures/commands/$default.ts +0 -5
- package/src/lib/fixtures/commands/create.ts +0 -6
- package/src/lib/fixtures/commands/db/health.ts +0 -9
- package/src/lib/fixtures/commands/db/migration/command.ts +0 -12
- package/src/lib/importCommand.test.ts +0 -60
- package/src/lib/importCommand.ts +0 -196
- package/src/lib/scanDirectory.test.ts +0 -75
- package/src/lib/scanDirectory.ts +0 -109
- package/src/lib/segmentPath.test.ts +0 -71
- 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(
|
|
33
|
+
const commandsDir = path.join(process.cwd(), 'dist/commands');
|
|
23
34
|
|
|
24
35
|
return yargs(hideBin(process.argv))
|
|
25
|
-
.scriptName(
|
|
26
|
-
.version(packageInfo.version!)
|
|
36
|
+
.scriptName('my-cli')
|
|
27
37
|
.command(
|
|
28
|
-
await fileCommands({ commandDirs: [commandsDir]
|
|
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
|
-
|
|
58
|
+
The above will result in these commands being registered:
|
|
47
59
|
|
|
48
|
-
```
|
|
49
|
-
|
|
60
|
+
```
|
|
61
|
+
db migration
|
|
62
|
+
db health
|
|
63
|
+
studio start
|
|
64
|
+
```
|
|
50
65
|
|
|
51
|
-
|
|
52
|
-
import type { BaseOptions } from '../options.js';
|
|
66
|
+
### 3. Define Commands
|
|
53
67
|
|
|
54
|
-
|
|
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
|
-
|
|
70
|
+
**Basic Command (`commands/studio.start.ts`)**
|
|
59
71
|
|
|
60
|
-
|
|
72
|
+
```ts
|
|
73
|
+
import { defineCommand } from 'yargs-file-commands';
|
|
61
74
|
|
|
62
|
-
export const
|
|
63
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
78
|
-
// Command with positional arguments
|
|
91
|
+
**Positional Arguments (`commands/create.ts`)**
|
|
79
92
|
|
|
80
|
-
|
|
81
|
-
|
|
93
|
+
```ts
|
|
94
|
+
import { defineCommand } from 'yargs-file-commands';
|
|
82
95
|
|
|
83
|
-
export const
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
111
|
+
**Default Command (`commands/$default.ts`)**
|
|
115
112
|
|
|
116
|
-
|
|
113
|
+
This command runs when no other command is specified.
|
|
117
114
|
|
|
118
115
|
```ts
|
|
119
|
-
import
|
|
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
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
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
|
-
|
|
156
|
+
pnpm install
|
|
191
157
|
|
|
192
158
|
# build everything
|
|
193
|
-
|
|
159
|
+
pnpm run build
|
|
194
160
|
|
|
195
|
-
#
|
|
196
|
-
|
|
161
|
+
# biome
|
|
162
|
+
pnpm run chec
|
|
197
163
|
|
|
198
|
-
#
|
|
199
|
-
|
|
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
|
-
|
|
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
package/dist/index.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC"}
|
package/dist/lib/Command.js
CHANGED
|
@@ -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
|
|
2
|
-
import {
|
|
3
|
-
|
|
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
|
-
|
|
30
|
+
expect(tree.length).toBe(1);
|
|
32
31
|
const rootNode = tree[0];
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
40
|
-
const childSegments = rootNode.children
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
+
expect(tree.length).toBe(1);
|
|
65
65
|
const node = tree[0];
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|