Skip to content

Commit f91683a

Browse files
boneskullclaude
andcommitted
feat(types): add TGlobalTransforms type parameter to CommandConfig
Enable command handlers created via bargs.command<>() to access properties added by global transforms. The curried form now accepts two type arguments: bargs.command<typeof globalOptions, typeof globalTransforms>()({ ... }) Changes: - Add TGlobalTransforms as second type parameter to CommandConfig (5 params now) - Update CommandBuilder interface and implementation to accept TGlobalTransforms - Update InferredCommands and CommandsInput to handle 5-parameter CommandConfig - Update handler type to apply global transforms before command transforms - Add TransformsInput fallback to CommandConfig.transforms for proper inference - Remove unused handler property from BargsConfig (non-command context) - Update examples/transforms.ts to demonstrate the new pattern with commands 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 4516aa6 commit f91683a

7 files changed

Lines changed: 222 additions & 238 deletions

File tree

examples/transforms.ts

Lines changed: 125 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
/**
33
* Transform example
44
*
5-
* Demonstrates how to use transforms to:
5+
* Demonstrates how to use transforms with both simple CLIs and commands:
66
*
7-
* - Load and merge config from a JSON file
8-
* - Add computed/derived values
9-
* - Transform positionals
7+
* - Global transforms that apply to all commands
8+
* - Command-specific transforms
9+
* - Computed/derived values flowing through handlers
1010
*
11-
* Usage: npx tsx examples/transforms.ts --verbose npx tsx
12-
* examples/transforms.ts --config config.json npx tsx examples/transforms.ts
13-
* file1.txt file2.txt
11+
* Usage: npx tsx examples/transforms.ts process file1.txt file2.txt --verbose
12+
* npx tsx examples/transforms.ts info --config config.json npx tsx
13+
* examples/transforms.ts --help
1414
*/
1515
import { existsSync, readFileSync } from 'node:fs';
1616

@@ -25,88 +25,137 @@ interface Config {
2525
verbose?: boolean;
2626
}
2727

28-
const main = async () => {
29-
/* eslint-disable perfectionist/sort-objects -- transforms must come before handler for type inference */
30-
const result = await bargsAsync({
31-
description: 'Demonstrates transforms feature',
32-
name: 'transforms-demo',
33-
options: {
34-
config: bargsAsync.string({
35-
aliases: ['c'],
36-
description: 'Path to JSON config file',
37-
}),
38-
outputDir: bargsAsync.string({
39-
aliases: ['o'],
40-
description: 'Output directory',
41-
}),
42-
verbose: bargsAsync.boolean({
43-
aliases: ['v'],
44-
default: false,
45-
description: 'Enable verbose output',
46-
}),
47-
},
48-
positionals: [
49-
bargsAsync.variadic('string', {
50-
description: 'Input files to process',
51-
name: 'files',
52-
}),
53-
],
54-
transforms: {
55-
positionals: (positionals) => {
56-
const [files] = positionals;
57-
const validFiles = files.filter((f) => {
28+
// Global options that will be available to all commands
29+
const globalOptions = {
30+
config: bargsAsync.string({
31+
aliases: ['c'],
32+
description: 'Path to JSON config file',
33+
}),
34+
outputDir: bargsAsync.string({
35+
aliases: ['o'],
36+
description: 'Output directory',
37+
}),
38+
verbose: bargsAsync.boolean({
39+
aliases: ['v'],
40+
default: false,
41+
description: 'Enable verbose output',
42+
}),
43+
} as const;
44+
45+
// Global transforms - these run before command transforms
46+
const globalTransforms = {
47+
values: (values: {
48+
config: string | undefined;
49+
outputDir: string | undefined;
50+
verbose: boolean;
51+
}) => {
52+
let fileConfig: Config = {};
53+
54+
if (values.config && existsSync(values.config)) {
55+
const content = readFileSync(values.config, 'utf8');
56+
fileConfig = JSON.parse(content) as Config;
57+
}
58+
59+
return {
60+
...fileConfig,
61+
...values,
62+
configLoaded: !!values.config,
63+
timestamp: new Date().toISOString(),
64+
};
65+
},
66+
} as const;
67+
68+
// Define commands using the typed command builder with global transforms
69+
// The second type argument passes global transforms for proper type inference
70+
const processCommand = bargsAsync.command<
71+
typeof globalOptions,
72+
typeof globalTransforms
73+
>()({
74+
description: 'Process files',
75+
handler: ({ positionals, values }) => {
76+
// Global transform properties are now available via TGlobalTransforms type arg
77+
const [files] = positionals;
78+
79+
if (values.verbose) {
80+
console.log('Processing configuration:', {
81+
configLoaded: values.configLoaded,
82+
outputDir: values.outputDir,
83+
timestamp: values.timestamp,
84+
verbose: values.verbose,
85+
});
86+
}
87+
88+
console.log(`Processing ${files.length} file(s):`);
89+
for (const file of files) {
90+
console.log(` - ${file.toUpperCase()}`); // Command transform uppercases
91+
}
92+
93+
if (values.outputDir) {
94+
console.log(`Output will be written to: ${values.outputDir}`);
95+
}
96+
},
97+
positionals: [
98+
bargsAsync.variadic('string', {
99+
description: 'Input files to process',
100+
name: 'files',
101+
}),
102+
],
103+
// Command-level transform - processes positionals
104+
transforms: {
105+
positionals: (positionals) => {
106+
const [files] = positionals;
107+
// Filter non-existent files and uppercase the rest
108+
const validFiles = files
109+
.filter((f) => {
58110
if (!existsSync(f)) {
59111
console.warn(`Warning: File not found: ${f}`);
60112
return false;
61113
}
62114
return true;
63-
});
64-
return [validFiles] as const;
65-
},
66-
values: (values) => {
67-
let fileConfig: Config = {};
115+
})
116+
.map((f) => f.toUpperCase());
117+
return [validFiles] as const;
118+
},
119+
},
120+
});
68121

69-
if (values.config && existsSync(values.config)) {
70-
const content = readFileSync(values.config, 'utf8');
71-
fileConfig = JSON.parse(content) as Config;
72-
}
122+
const infoCommand = bargsAsync.command<
123+
typeof globalOptions,
124+
typeof globalTransforms
125+
>()({
126+
description: 'Show configuration info',
127+
handler: ({ values }) => {
128+
// Global transform properties are available via TGlobalTransforms type arg
129+
console.log('Current configuration:');
130+
console.log(` Config file: ${values.config ?? '(none)'}`);
131+
console.log(` Output dir: ${values.outputDir ?? '(default)'}`);
132+
console.log(` Verbose: ${values.verbose}`);
133+
console.log(` Config loaded: ${values.configLoaded}`);
134+
console.log(` Timestamp: ${values.timestamp}`);
135+
},
136+
});
73137

74-
return {
75-
...fileConfig,
76-
...values,
77-
configLoaded: !!values.config,
78-
timestamp: new Date().toISOString(),
79-
};
80-
},
138+
const main = async () => {
139+
await bargsAsync({
140+
commands: {
141+
info: infoCommand,
142+
process: processCommand,
81143
},
82-
handler: ({ positionals, values }) => {
83-
const [files] = positionals;
84-
144+
defaultHandler: ({ values }) => {
145+
console.log('No command specified. Use --help for usage.');
85146
if (values.verbose) {
86-
console.log('Configuration:', {
87-
configLoaded: values.configLoaded,
88-
outputDir: values.outputDir,
89-
timestamp: values.timestamp,
90-
verbose: values.verbose,
91-
});
92-
}
93-
94-
console.log(`Processing ${files.length} file(s):`);
95-
for (const file of files) {
96-
console.log(` - ${file}`);
97-
}
98-
99-
if (values.outputDir) {
100-
console.log(`Output will be written to: ${values.outputDir}`);
147+
console.log('(verbose mode enabled)');
101148
}
149+
// Test: Can we access global transform-added properties?
150+
console.log(`Config loaded: ${values.configLoaded}`);
151+
console.log(`Timestamp: ${values.timestamp}`);
102152
},
153+
description: 'Demonstrates transforms with commands',
154+
name: 'transforms-demo',
155+
options: globalOptions,
156+
transforms: globalTransforms,
103157
version: '1.0.0',
104158
});
105-
/* eslint-enable perfectionist/sort-objects */
106-
107-
if (result.values.verbose) {
108-
console.log('\nFinal result:', result);
109-
}
110159
};
111160

112161
void main();

src/bargs.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ import {
3232
parseCommandsAsync,
3333
parseCommandsSync,
3434
parseSimple,
35-
runHandler,
36-
runSyncHandler,
3735
runSyncTransforms,
3836
runTransforms,
3937
} from './parser.js';
@@ -242,11 +240,6 @@ export function bargs(
242240
values: transformed.values,
243241
} as BargsResult<unknown, readonly unknown[], undefined>;
244242

245-
// Call handler if provided (sync)
246-
if (config.handler) {
247-
runSyncHandler(config.handler as (r: typeof result) => void, result);
248-
}
249-
250243
return result;
251244
}
252245
} catch (error) {
@@ -347,14 +340,6 @@ export async function bargsAsync(
347340
values: transformed.values,
348341
} as BargsResult<unknown, readonly unknown[], undefined>;
349342

350-
// Call handler if provided (async)
351-
if (config.handler) {
352-
await runHandler(
353-
config.handler as (r: typeof result) => Promise<void> | void,
354-
result,
355-
);
356-
}
357-
358343
return result;
359344
}
360345
} catch (error) {

0 commit comments

Comments
 (0)