core icon indicating copy to clipboard operation
core copied to clipboard

New flag Constraints API Proposal

Open AllanOricil opened this issue 5 months ago • 7 comments

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

  1. 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
  1. Manual validation in run()
  • extremely verbose
  • not reusable
  • bypasses Oclif’s declarative philosophy error messages inconsistent across commands
  1. 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);
  }
}

AllanOricil avatar Nov 18 '25 21:11 AllanOricil

Take the Opportunity to declare defineFlags to ensure typesafety. It accepts an object with props of Flag types.

AllanOricil avatar Nov 26 '25 16:11 AllanOricil

@mdonnalley @cristiand391 @iowillhoit @WillieRuemmele what do you guys think about this new api for v5?

AllanOricil avatar Dec 03 '25 15:12 AllanOricil

This new API + bun build will make oclif CLIs way cooler

AllanOricil avatar Dec 07 '25 00:12 AllanOricil

I feel I'm being ignored 🙁

AllanOricil avatar Dec 09 '25 23:12 AllanOricil

@AllanOricil , thank you very much for your patience. The team is definitely intrigued by this idea, and we're currently in the process of talking it over and hammering out the specifics. We'll keep you posted with updates as and when they become available.

jfeingold35 avatar Dec 10 '25 16:12 jfeingold35

@AllanOricil . The team has decided that this is a great idea, and we're going to move forward with it. We won't be implementing it exactly as you proposed, but it'll be something very much like this proposal, and we're excited for you to get to play with it. We'll be adding it to our backlog, and we'll continue providing updates as they become available. Thanks for the suggestion!

jfeingold35 avatar Dec 12 '25 15:12 jfeingold35

This issue has been linked to a new work item: W-20534184

git2gus[bot] avatar Dec 12 '25 15:12 git2gus[bot]

With flags, args, and constraints available directly in the run method’s context, testing becomes significantly more straightforward. We can assert outcomes in isolation, without coupling tests to parsing or constraint-validation logic. This enables precise injection of inputs and focused assertions on the run method’s behavior itself. The proposed testing approach is illustrated in the accompanying proposal.

https://github.com/oclif/core/issues/1520

AllanOricil avatar Dec 27 '25 22:12 AllanOricil