clap-ts 0.3.0 → 0.4.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/dist/parser.js +5 -5
- package/dist/types.d.ts +14 -1
- package/package.json +17 -1
- package/src/__tests__/arg-options.test.ts +687 -0
- package/src/__tests__/argfile.test.ts +127 -0
- package/src/__tests__/clap-parity.test.ts +682 -0
- package/src/__tests__/command-options.test.ts +713 -0
- package/src/__tests__/completions.test.ts +423 -0
- package/src/__tests__/config.test.ts +261 -0
- package/src/__tests__/deprecation.test.ts +104 -0
- package/src/__tests__/help.test.ts +312 -0
- package/src/__tests__/install.test.ts +120 -0
- package/src/__tests__/log.test.ts +189 -0
- package/src/__tests__/man.test.ts +135 -0
- package/src/__tests__/markdown.test.ts +114 -0
- package/src/__tests__/output.test.ts +249 -0
- package/src/__tests__/parser.test.ts +627 -0
- package/src/__tests__/plugins.test.ts +182 -0
- package/src/__tests__/progress.test.ts +221 -0
- package/src/__tests__/prompt.test.ts +265 -0
- package/src/__tests__/runner.test.ts +459 -0
- package/src/__tests__/spec.test.ts +107 -0
- package/src/__tests__/testing.test.ts +93 -0
- package/src/__tests__/validation.test.ts +267 -0
- package/src/argfile.ts +188 -0
- package/src/completions.ts +865 -0
- package/src/config.ts +184 -0
- package/src/help.ts +779 -0
- package/src/index.ts +58 -0
- package/src/install.ts +226 -0
- package/src/log.ts +225 -0
- package/src/man.ts +289 -0
- package/src/markdown.ts +210 -0
- package/src/output.ts +453 -0
- package/src/parser.ts +1240 -0
- package/src/plugins.ts +193 -0
- package/src/progress.ts +295 -0
- package/src/prompt.ts +388 -0
- package/src/runner.ts +769 -0
- package/src/spec.ts +197 -0
- package/src/testing.ts +159 -0
- package/src/types.ts +618 -0
- package/src/validation.ts +627 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for deprecating arguments and commands.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect } from 'bun:test';
|
|
6
|
+
import { parseArgs } from '../parser.js';
|
|
7
|
+
import { renderHelp } from '../help.js';
|
|
8
|
+
import { defineCommand } from '../runner.js';
|
|
9
|
+
import { runCli, captureArgs, stripAnsi } from '../testing.js';
|
|
10
|
+
import type { CommandDef } from '../types.js';
|
|
11
|
+
|
|
12
|
+
describe('deprecated arguments', () => {
|
|
13
|
+
const command = defineCommand({
|
|
14
|
+
meta: { name: 'app' },
|
|
15
|
+
args: {
|
|
16
|
+
old: { type: 'string', deprecated: true, description: 'Old flag' },
|
|
17
|
+
stale: { type: 'string', deprecated: 'no longer read', description: 'Stale flag' },
|
|
18
|
+
current: { type: 'string', description: 'Current flag' },
|
|
19
|
+
legacy: { type: 'string', deprecated: true, replacedBy: 'current' },
|
|
20
|
+
},
|
|
21
|
+
run() {},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('using one warns on stderr', async () => {
|
|
25
|
+
const { plainStderr } = await runCli(command, ['--old', 'x']);
|
|
26
|
+
expect(plainStderr).toContain("warning: '--old' is deprecated");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('a string deprecation carries its reason', async () => {
|
|
30
|
+
const { plainStderr } = await runCli(command, ['--stale', 'x']);
|
|
31
|
+
expect(plainStderr).toContain("'--stale' is deprecated: no longer read");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('replacedBy names the replacement in the warning', async () => {
|
|
35
|
+
const { plainStderr } = await runCli(command, ['--legacy', 'x']);
|
|
36
|
+
expect(plainStderr).toContain("use '--current' instead");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('replacedBy forwards the value', async () => {
|
|
40
|
+
const { args } = await captureArgs(command, ['--legacy', 'x']);
|
|
41
|
+
expect(args['current']).toBe('x');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('an explicit replacement wins over the forwarded value', async () => {
|
|
45
|
+
const { args } = await captureArgs(command, ['--legacy', 'old', '--current', 'new']);
|
|
46
|
+
expect(args['current']).toBe('new');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('a current argument warns about nothing', async () => {
|
|
50
|
+
const { plainStderr } = await runCli(command, ['--current', 'x']);
|
|
51
|
+
expect(plainStderr).toBe('');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('the warning does not fail the run', async () => {
|
|
55
|
+
const { exitCode } = await runCli(command, ['--old', 'x']);
|
|
56
|
+
expect(exitCode).toBe(0);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('it warns once even when repeated', () => {
|
|
60
|
+
const appendable: CommandDef = {
|
|
61
|
+
meta: { name: 'app' },
|
|
62
|
+
args: { old: { type: 'string', action: 'append', deprecated: true } },
|
|
63
|
+
};
|
|
64
|
+
expect(parseArgs(['--old', 'a', '--old', 'b'], appendable).warnings).toHaveLength(1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('help labels it', () => {
|
|
68
|
+
const help = stripAnsi(renderHelp(command));
|
|
69
|
+
expect(help).toContain('[deprecated]');
|
|
70
|
+
expect(help).toContain('[deprecated: no longer read]');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('deprecated commands', () => {
|
|
75
|
+
const root = defineCommand({
|
|
76
|
+
meta: { name: 'app' },
|
|
77
|
+
subCommands: {
|
|
78
|
+
publish: defineCommand({ meta: { name: 'publish', description: 'Publish' }, run() {} }),
|
|
79
|
+
push: defineCommand({
|
|
80
|
+
meta: {
|
|
81
|
+
name: 'push',
|
|
82
|
+
description: 'Push',
|
|
83
|
+
deprecated: 'renamed',
|
|
84
|
+
replacedBy: 'publish',
|
|
85
|
+
},
|
|
86
|
+
run() {},
|
|
87
|
+
}),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('running one warns', async () => {
|
|
92
|
+
const { plainStderr, exitCode } = await runCli(root, ['push']);
|
|
93
|
+
expect(plainStderr).toContain("warning: 'push' is deprecated: renamed; use 'publish' instead");
|
|
94
|
+
expect(exitCode).toBe(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('the current command is quiet', async () => {
|
|
98
|
+
expect((await runCli(root, ['publish'])).plainStderr).toBe('');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('help labels it in the command list', () => {
|
|
102
|
+
expect(stripAnsi(renderHelp(root))).toContain('[deprecated: renamed]');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Help renderer tests - tests for clap-style help output generation.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect } from 'bun:test';
|
|
6
|
+
import { renderHelp } from '../help.js';
|
|
7
|
+
import { stripAnsi } from '../testing.js';
|
|
8
|
+
import type { ArgsDef, CommandDef } from '../types.js';
|
|
9
|
+
|
|
10
|
+
/** Strip ANSI escape sequences for assertion on content. */
|
|
11
|
+
function cmd(args: ArgsDef, extra?: Partial<CommandDef>): CommandDef {
|
|
12
|
+
return { meta: { name: 'test' }, args, ...extra };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const basicCommand: CommandDef = {
|
|
16
|
+
meta: {
|
|
17
|
+
name: 'my-tool',
|
|
18
|
+
version: '1.0.0',
|
|
19
|
+
description: 'A great tool',
|
|
20
|
+
about: 'A tool that does great things',
|
|
21
|
+
},
|
|
22
|
+
args: {
|
|
23
|
+
verbose: {
|
|
24
|
+
type: 'boolean',
|
|
25
|
+
short: 'v',
|
|
26
|
+
description: 'Enable verbose output',
|
|
27
|
+
},
|
|
28
|
+
port: {
|
|
29
|
+
type: 'number',
|
|
30
|
+
short: 'p',
|
|
31
|
+
default: 3000,
|
|
32
|
+
valueName: 'PORT',
|
|
33
|
+
description: 'Port to listen on',
|
|
34
|
+
},
|
|
35
|
+
env: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
short: 'e',
|
|
38
|
+
env: 'MY_TOOL_ENV',
|
|
39
|
+
valueParser: ['dev', 'staging', 'prod'],
|
|
40
|
+
description: 'Environment to use',
|
|
41
|
+
},
|
|
42
|
+
file: {
|
|
43
|
+
type: 'positional',
|
|
44
|
+
valueName: 'FILE',
|
|
45
|
+
required: true,
|
|
46
|
+
description: 'Input file',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
describe('help rendering', () => {
|
|
52
|
+
test('includes all sections', () => {
|
|
53
|
+
const help = stripAnsi(renderHelp(basicCommand));
|
|
54
|
+
// Header with name and version
|
|
55
|
+
expect(help).toContain('my-tool');
|
|
56
|
+
expect(help).toContain('v1.0.0');
|
|
57
|
+
// About text
|
|
58
|
+
expect(help).toContain('A tool that does great things');
|
|
59
|
+
// Usage line
|
|
60
|
+
expect(help).toContain('Usage:');
|
|
61
|
+
expect(help).toContain('my-tool');
|
|
62
|
+
// Arguments section
|
|
63
|
+
expect(help).toContain('Arguments:');
|
|
64
|
+
expect(help).toContain('FILE');
|
|
65
|
+
// Options section
|
|
66
|
+
expect(help).toContain('Options:');
|
|
67
|
+
expect(help).toContain('--verbose');
|
|
68
|
+
expect(help).toContain('--port');
|
|
69
|
+
expect(help).toContain('--env');
|
|
70
|
+
// Built-in flags
|
|
71
|
+
expect(help).toContain('--help');
|
|
72
|
+
expect(help).toContain('--version');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('aliases shown in help (subcommands)', () => {
|
|
76
|
+
const command: CommandDef = {
|
|
77
|
+
meta: { name: 'app' },
|
|
78
|
+
subCommands: {
|
|
79
|
+
serve: {
|
|
80
|
+
meta: { name: 'serve', description: 'Start server', aliases: ['s', 'start'] },
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
const help = stripAnsi(renderHelp(command));
|
|
85
|
+
expect(help).toContain('serve');
|
|
86
|
+
expect(help).toContain('s, start');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('default values shown', () => {
|
|
90
|
+
const help = stripAnsi(renderHelp(basicCommand));
|
|
91
|
+
expect(help).toContain('[default: 3000]');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('env var names shown', () => {
|
|
95
|
+
const help = stripAnsi(renderHelp(basicCommand));
|
|
96
|
+
expect(help).toContain('[env: MY_TOOL_ENV]');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('required markers shown for positionals', () => {
|
|
100
|
+
const help = stripAnsi(renderHelp(basicCommand));
|
|
101
|
+
// Required positionals appear as <FILE> in usage line
|
|
102
|
+
expect(help).toContain('<FILE>');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('possible values shown', () => {
|
|
106
|
+
const help = stripAnsi(renderHelp(basicCommand));
|
|
107
|
+
// Help text may wrap across lines; normalize whitespace for matching
|
|
108
|
+
const normalized = help.replaceAll(/\s+/g, ' ');
|
|
109
|
+
expect(normalized).toContain('[possible values: dev, staging, prod]');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('short flags shown', () => {
|
|
113
|
+
const help = stripAnsi(renderHelp(basicCommand));
|
|
114
|
+
expect(help).toContain('-v');
|
|
115
|
+
expect(help).toContain('-p');
|
|
116
|
+
expect(help).toContain('-e');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('hidden args not shown', () => {
|
|
120
|
+
const command: CommandDef = {
|
|
121
|
+
meta: { name: 'tool' },
|
|
122
|
+
args: {
|
|
123
|
+
visible: { type: 'boolean', description: 'Visible flag' },
|
|
124
|
+
secret: { type: 'boolean', description: 'Secret flag', hidden: true },
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
const help = stripAnsi(renderHelp(command));
|
|
128
|
+
expect(help).toContain('--visible');
|
|
129
|
+
expect(help).not.toContain('--secret');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('subcommands section rendered', () => {
|
|
133
|
+
const command: CommandDef = {
|
|
134
|
+
meta: { name: 'app' },
|
|
135
|
+
subCommands: {
|
|
136
|
+
serve: { meta: { name: 'serve', description: 'Start the server' } },
|
|
137
|
+
build: { meta: { name: 'build', description: 'Build the project' } },
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
const help = stripAnsi(renderHelp(command));
|
|
141
|
+
expect(help).toContain('Commands:');
|
|
142
|
+
expect(help).toContain('serve');
|
|
143
|
+
expect(help).toContain('Start the server');
|
|
144
|
+
expect(help).toContain('build');
|
|
145
|
+
expect(help).toContain('Build the project');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('after help text shown', () => {
|
|
149
|
+
const command: CommandDef = {
|
|
150
|
+
meta: { name: 'tool', afterHelp: 'For more info visit https://example.com' },
|
|
151
|
+
};
|
|
152
|
+
const help = stripAnsi(renderHelp(command));
|
|
153
|
+
expect(help).toContain('For more info visit https://example.com');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('optional value indicator shown for numArgs.min=0', () => {
|
|
157
|
+
const command: CommandDef = {
|
|
158
|
+
meta: { name: 'tool' },
|
|
159
|
+
args: {
|
|
160
|
+
level: {
|
|
161
|
+
type: 'string',
|
|
162
|
+
valueName: 'LEVEL',
|
|
163
|
+
numArgs: { min: 0, max: 1 },
|
|
164
|
+
description: 'Log level',
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
const help = stripAnsi(renderHelp(command));
|
|
169
|
+
// Optional values shown as [LEVEL] instead of <LEVEL>
|
|
170
|
+
expect(help).toContain('[LEVEL]');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('hidePossibleValues', () => {
|
|
175
|
+
test('hides possible values from help', () => {
|
|
176
|
+
const command = cmd({
|
|
177
|
+
env: {
|
|
178
|
+
type: 'string',
|
|
179
|
+
long: 'env',
|
|
180
|
+
valueParser: ['dev', 'staging', 'prod'],
|
|
181
|
+
hidePossibleValues: true,
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
const help = stripAnsi(renderHelp(command));
|
|
185
|
+
expect(help).not.toContain('possible values');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('shows possible values by default', () => {
|
|
189
|
+
const command = cmd({
|
|
190
|
+
env: {
|
|
191
|
+
type: 'string',
|
|
192
|
+
long: 'env',
|
|
193
|
+
valueParser: ['dev', 'staging', 'prod'],
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
const help = stripAnsi(renderHelp(command));
|
|
197
|
+
expect(help).toContain('possible values');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('hideShortHelp / hideLongHelp', () => {
|
|
202
|
+
test('hideShortHelp hides from -h but shows in --help', () => {
|
|
203
|
+
const command = cmd({
|
|
204
|
+
advanced: { type: 'boolean', long: 'advanced', description: 'Advanced mode', hideShortHelp: true },
|
|
205
|
+
basic: { type: 'boolean', long: 'basic', description: 'Basic mode' },
|
|
206
|
+
});
|
|
207
|
+
const shortHelp = stripAnsi(renderHelp(command, undefined, true));
|
|
208
|
+
const longHelp = stripAnsi(renderHelp(command, undefined, false));
|
|
209
|
+
expect(shortHelp).not.toContain('--advanced');
|
|
210
|
+
expect(longHelp).toContain('--advanced');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('hideLongHelp hides from --help but shows in -h', () => {
|
|
214
|
+
const command = cmd({
|
|
215
|
+
debug: { type: 'boolean', long: 'debug', description: 'Debug mode', hideLongHelp: true },
|
|
216
|
+
});
|
|
217
|
+
const shortHelp = stripAnsi(renderHelp(command, undefined, true));
|
|
218
|
+
const longHelp = stripAnsi(renderHelp(command, undefined, false));
|
|
219
|
+
expect(shortHelp).toContain('--debug');
|
|
220
|
+
expect(longHelp).not.toContain('--debug');
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('visibleAlias in help', () => {
|
|
225
|
+
test('visible aliases shown in help output', () => {
|
|
226
|
+
const command = cmd({
|
|
227
|
+
output: { type: 'string', long: 'output', visibleAlias: ['out', 'o'], description: 'Output path' },
|
|
228
|
+
});
|
|
229
|
+
const help = stripAnsi(renderHelp(command));
|
|
230
|
+
expect(help).toContain('--out');
|
|
231
|
+
expect(help).toContain('-o');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('hidden aliases not shown in help', () => {
|
|
235
|
+
const command = cmd({
|
|
236
|
+
output: { type: 'string', long: 'output', alias: ['out'], description: 'Output path' },
|
|
237
|
+
});
|
|
238
|
+
const help = stripAnsi(renderHelp(command));
|
|
239
|
+
// Check that --out doesn't appear as a standalone flag display
|
|
240
|
+
// (--output contains --out as substring, so use regex for exact match)
|
|
241
|
+
expect(help).not.toMatch(/,\s*--out\b/);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('beforeHelp', () => {
|
|
246
|
+
test('renders text before help output', () => {
|
|
247
|
+
const command: CommandDef = {
|
|
248
|
+
meta: { name: 'tool', beforeHelp: 'WARNING: This tool is experimental.' },
|
|
249
|
+
};
|
|
250
|
+
const help = stripAnsi(renderHelp(command));
|
|
251
|
+
expect(help.indexOf('WARNING')).toBeLessThan(help.indexOf('Usage:'));
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe('helpHeading', () => {
|
|
256
|
+
test('groups args under custom headings', () => {
|
|
257
|
+
const command = cmd({
|
|
258
|
+
verbose: { type: 'boolean', long: 'verbose', description: 'Verbose' },
|
|
259
|
+
host: { type: 'string', long: 'host', description: 'Host', helpHeading: 'Network' },
|
|
260
|
+
port: { type: 'number', long: 'port', description: 'Port', helpHeading: 'Network' },
|
|
261
|
+
});
|
|
262
|
+
const help = stripAnsi(renderHelp(command));
|
|
263
|
+
expect(help).toContain('Options:');
|
|
264
|
+
expect(help).toContain('Network:');
|
|
265
|
+
// verbose under Options, host/port under Network
|
|
266
|
+
const optionsIdx = help.indexOf('Options:');
|
|
267
|
+
const networkIdx = help.indexOf('Network:');
|
|
268
|
+
const verboseIdx = help.indexOf('--verbose');
|
|
269
|
+
const hostIdx = help.indexOf('--host');
|
|
270
|
+
expect(verboseIdx).toBeGreaterThan(optionsIdx);
|
|
271
|
+
expect(verboseIdx).toBeLessThan(networkIdx);
|
|
272
|
+
expect(hostIdx).toBeGreaterThan(networkIdx);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe('helpTemplate', () => {
|
|
277
|
+
test('custom template replaces default rendering', () => {
|
|
278
|
+
const command: CommandDef = {
|
|
279
|
+
meta: {
|
|
280
|
+
name: 'tool',
|
|
281
|
+
version: '1.0.0',
|
|
282
|
+
helpTemplate: 'NAME: {name}\nVERSION: {version}\n{usage}\n{options}',
|
|
283
|
+
},
|
|
284
|
+
args: {
|
|
285
|
+
verbose: { type: 'boolean', long: 'verbose', description: 'Verbose' },
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
const help = stripAnsi(renderHelp(command));
|
|
289
|
+
expect(help).toContain('NAME: tool');
|
|
290
|
+
expect(help).toContain('VERSION: 1.0.0');
|
|
291
|
+
expect(help).toContain('Usage:');
|
|
292
|
+
expect(help).toContain('--verbose');
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe('custom styles', () => {
|
|
297
|
+
test('style overrides are applied to help', () => {
|
|
298
|
+
const command: CommandDef = {
|
|
299
|
+
meta: { name: 'tool', version: '1.0.0' },
|
|
300
|
+
args: {
|
|
301
|
+
verbose: { type: 'boolean', long: 'verbose', description: 'Verbose' },
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
const customStyles = {
|
|
305
|
+
heading: (s: string) => `[H]${s}[/H]`,
|
|
306
|
+
flag: (s: string) => `[F]${s}[/F]`,
|
|
307
|
+
};
|
|
308
|
+
const help = renderHelp(command, undefined, false, customStyles);
|
|
309
|
+
expect(help).toContain('[H]Usage:[/H]');
|
|
310
|
+
expect(help).toContain('[F]--verbose[/F]');
|
|
311
|
+
});
|
|
312
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for installing completions and man pages.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect, afterEach, beforeEach } from 'bun:test';
|
|
6
|
+
import { mkdtempSync, rmSync, readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { completionTarget, installCompletions, installManPages, withInstallers } from '../install.js';
|
|
10
|
+
import { defineCommand } from '../runner.js';
|
|
11
|
+
import { runCli } from '../testing.js';
|
|
12
|
+
import type { Shell } from '../types.js';
|
|
13
|
+
|
|
14
|
+
const main = defineCommand({
|
|
15
|
+
meta: { name: 'demo', version: '1.0', description: 'A demo' },
|
|
16
|
+
args: { verbose: { type: 'boolean', short: 'v', description: 'Verbose' } },
|
|
17
|
+
subCommands: { serve: defineCommand({ meta: { name: 'serve', description: 'Serve' }, run() {} }) },
|
|
18
|
+
run() {},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
let dir: string;
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
dir = mkdtempSync(join(tmpdir(), 'clap-ts-install-'));
|
|
24
|
+
});
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
rmSync(dir, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('completionTarget', () => {
|
|
30
|
+
test('names the file the way each shell expects', () => {
|
|
31
|
+
expect(completionTarget('bash', 'demo').file).toBe('demo');
|
|
32
|
+
expect(completionTarget('zsh', 'demo').file).toBe('_demo');
|
|
33
|
+
expect(completionTarget('fish', 'demo').file).toBe('demo.fish');
|
|
34
|
+
expect(completionTarget('nushell', 'demo').file).toBe('demo.nu');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('reports a manual step only where sourcing is not automatic', () => {
|
|
38
|
+
expect(completionTarget('bash', 'demo').manualStep).toBeUndefined();
|
|
39
|
+
expect(completionTarget('fish', 'demo').manualStep).toBeUndefined();
|
|
40
|
+
expect(completionTarget('zsh', 'demo').manualStep).toContain('fpath');
|
|
41
|
+
expect(completionTarget('powershell', 'demo').manualStep).toContain('$PROFILE');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('installCompletions', () => {
|
|
46
|
+
test('writes the script and reports the path', () => {
|
|
47
|
+
const result = installCompletions(main, 'bash', { dir });
|
|
48
|
+
expect(result.paths).toEqual([join(dir, 'demo')]);
|
|
49
|
+
expect(readFileSync(result.paths[0]!, 'utf8')).toContain('# bash completion for demo');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('a dry run reports the path and writes nothing', () => {
|
|
53
|
+
const result = installCompletions(main, 'bash', { dir: join(dir, 'nested'), dryRun: true });
|
|
54
|
+
expect(result.dryRun).toBe(true);
|
|
55
|
+
expect(existsSync(join(dir, 'nested'))).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('creates the directory when it is missing', () => {
|
|
59
|
+
installCompletions(main, 'fish', { dir: join(dir, 'a', 'b') });
|
|
60
|
+
expect(existsSync(join(dir, 'a', 'b', 'demo.fish'))).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('every shell installs', () => {
|
|
64
|
+
for (const shell of ['bash', 'zsh', 'fish', 'powershell', 'elvish', 'nushell'] as Shell[]) {
|
|
65
|
+
const result = installCompletions(main, shell, { dir: join(dir, shell) });
|
|
66
|
+
expect(readFileSync(result.paths[0]!, 'utf8').length).toBeGreaterThan(0);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('installManPages', () => {
|
|
72
|
+
test('writes one page per command', () => {
|
|
73
|
+
const result = installManPages(main, { dir });
|
|
74
|
+
expect(readdirSync(dir).sort()).toEqual(['demo-serve.1', 'demo.1']);
|
|
75
|
+
expect(result.paths).toHaveLength(2);
|
|
76
|
+
expect(readFileSync(join(dir, 'demo.1'), 'utf8')).toContain('.TH demo 1');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('a dry run writes nothing', () => {
|
|
80
|
+
installManPages(main, { dir: join(dir, 'nested'), dryRun: true });
|
|
81
|
+
expect(existsSync(join(dir, 'nested'))).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('withInstallers', () => {
|
|
86
|
+
const wrapped = withInstallers(main);
|
|
87
|
+
|
|
88
|
+
test('printing still works', async () => {
|
|
89
|
+
const { stdout } = await runCli(wrapped, ['completions', 'bash']);
|
|
90
|
+
expect(stdout).toContain('# bash completion for demo');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('man prints roff', async () => {
|
|
94
|
+
const { stdout } = await runCli(wrapped, ['man']);
|
|
95
|
+
expect(stdout).toContain('.TH demo 1');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('install reports what it would do', async () => {
|
|
99
|
+
const { stdout, exitCode } = await runCli(wrapped, [
|
|
100
|
+
'completions',
|
|
101
|
+
'install',
|
|
102
|
+
'zsh',
|
|
103
|
+
'--dryRun',
|
|
104
|
+
]);
|
|
105
|
+
expect(exitCode).toBe(0);
|
|
106
|
+
expect(stdout).toContain('would write');
|
|
107
|
+
expect(stdout).toContain('_demo');
|
|
108
|
+
expect(stdout).toContain('note:');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('an unknown shell is rejected with the valid ones listed', async () => {
|
|
112
|
+
const { plainStderr, exitCode } = await runCli(wrapped, ['completions', 'nope']);
|
|
113
|
+
expect(exitCode).toBe(2);
|
|
114
|
+
expect(plainStderr).toContain('possible values: bash, zsh, fish, powershell, elvish, nushell');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('the original subcommands survive', async () => {
|
|
118
|
+
expect(wrapped.subCommands!['serve']).toBeDefined();
|
|
119
|
+
});
|
|
120
|
+
});
|