-
Notifications
You must be signed in to change notification settings - Fork 83
Description
Please, before disregarding this idea, validate its pros/cons with other cli developers/builders.
Is your feature request related to a problem? Please describe.
Oclif’s current way of declaring command constraints, using flag atLeastOne, relationships, dependsOn, exclusive, exactlyOne and compatible, forces developers to encode validation rules inside individual flags, which fragments business logic across multiple definitions and increases cognitive load.
This results in:
- duplicated logic (exclusive has to be added in multiple places)
- impossible constraints (e.g., true XOR / “one-of-required”)
- scattered dependency rules (dependsOn, relationships)
- maintenance overhead (one new flag often forces rewriting several others)
- unintuitive validation behavior when multiple rules interact, specially when using
relationships
For anything beyond trivial commands, developers end up manually re-implementing logic inside run(), defeating the purpose of declarative flag relationships.
This creates real friction and makes flag-heavy commands difficult to maintain at scale.
Describe the solution you'd like
Introduce a centralized defineConstraints DSL that declares all flag relationships in a single, declarative structure. This shifts the mental model from “each flag owns its own rules” to “the command defines the rules; flags define types.”
This focuses the developer on business logic instead of scattering rules across the flag map.
Proposed defineConstraints DSL
Example 1 — Simple and readable
import { xor, oneOf, require, forbid, defineConstraints } from '@oclif/constraints';
static constraints = defineConstraints([
xor('upload', 'download').required(),
oneOf('json', 'xml').required(),
require('token').when('login'),
forbid('debug').when('production'),
]);This DSL compiles into a clear, predictable validation schema (serializable) without the developer needing to understand complex internals.
Example 2 — Mixed DSL + schema hybrid (supported)
static constraints = defineConstraints([
xor('a', 'b').required(),
{
allOf: [['region', 'accountId']]
}
]);Both syntaxes are supported. DSL is ergonomic; schema is serializable = DSL turns into a structure json that can be serialized.
Describe alternatives you've considered
- Current “relationships” API
- exclusive can prevent two flags from being used together but cannot require that one of them is present
- dependsOn cannot express grouped conditions
- implies cannot express mutual requirements or XOR
- all rules must be duplicated across flags
leads to contradictory or incomplete rule sets
- Manual validation in run()
- extremely verbose
- not reusable
- bypasses Oclif’s declarative philosophy
error messages inconsistent across commands
- Adding more relationships to flags
- makes flags bloated
- increases cognitive load
- does not fix the fundamental issue: rules belong to commands, not flags
Additional context
Centralization eliminates cognitive load
Today, developers must bounce between 5–10 flags to reconstruct what the command actually expects. With a constraints DSL:
- logic lives in one place
- business rules are explicit
- onboarding new maintainers is trivial
- changes require updating only one file and one structure
The DSL is forward-thinking
It opens the door for:
- auto-generated docs
- static analysis
- validation previews
- better error messages
- cross-command rule sharing
Flags become clean
No more:
required: true
exclusive: ['x', 'y']
dependsOn: ['token']Flags declare shape and all the other flag api props that aren't related to validations:
Flags.string({ description: "foo", default: "foo"})
Flags.boolean()
Flags.integer()All logic moves to constraints.
Example
import {
Command,
Flags,
//NOTE: you could add these constraints specific methods under the Constraints namespace inside core to simplify
xor,
oneOf,
atLeast,
require,
forbid,
custom,
defineFlags,
defineConstraints
} from '@oclif/core';
export default class Deploy extends Command {
static description = 'Deploy an application to the selected environment';
//NOTE: new method called defineFlags to ensure typesafety
static flags = defineFlags({
//NOTE: Deployment target
app: Flags.string({ description: 'app name' }),
service: Flags.string({ description: 'service name' }),
//NOTE: Output format
json: Flags.boolean(),
yaml: Flags.boolean(),
//NOTE: AWS details
region: Flags.string(),
profile: Flags.string(),
accessKey: Flags.string(),
secretKey: Flags.string(),
//NOTE: Options
dryRun: Flags.boolean(),
verbose: Flags.boolean(),
pretty: Flags.boolean(),
});
// NOTE: centralize validation rules / command constraints
static constraints = defineConstraints([
//NOTE: Require exactly one deploy target
xor('app', 'service').required().withError('Specify exactly one deploy target: --app or --service'),
//NOTE: Output formats: one required
oneOf('json', 'yaml').required(),
//NOTE: AWS: require either profile OR (accessKey + secretKey)
require('profile').whenNone('accessKey', 'secretKey'),
require(['accessKey', 'secretKey']).whenNone('profile'),
//NOTE: Pretty-print only allowed with YAML
forbid('pretty').when('json'),
//NOTE: Verbose allowed only outside dry-run mode
forbid('verbose').when('dryRun'),
//NOTE: Require region for any deploy
require('region').whenAny('app', 'service'),
//NOTE: At least one tag if YAML output
atLeast('tag', 1).when('yaml'),
//NOTE: Custom edge-case rule
custom(({ flags }) => {
if (flags.accessKey && !flags.secretKey) {
return 'Using --access-key requires --secret-key';
}
}),
]);
//NOTE: By this point, all validations are already verified. That is why parsed flags and constraints results are available in the context of run
async run({ flags, constraints }) {
//NOTE: no longer needed because the core has already parsed flags and made it available in the run context.
//const { flags } = await this.parse(Deploy);
this.log('Deployment starting with flags:', flags);
}
}