Skip to content

Conversation

@mdonnalley
Copy link
Contributor

POC of a generating a flag graph that can then be used to iteratively serve prompts to the user

Example Implementation:

import {confirm, input, select} from '@inquirer/prompts'
import {Command, Flags, PromptGenerator} from '@oclif/core'
import {inspect} from 'node:util'

export default class World extends Command {
  static args = {}
  static description = 'Say hello world and test flag dependencies'
  static examples = [`<%= config.bin %> <%= command.id %>`]
  static flags = {
    // 'when' condition
    'conditional-a': Flags.string({
      description: "Depends on conditional-b when conditional-trigger is 'met'.",
      relationships: [
        {
          flags: [
            {
              dependencies: ['conditional-trigger'],
              name: 'conditional-b',
              when: async (flags) => flags['conditional-trigger'] === 'met',
            },
          ],
          type: 'all',
        },
      ],
    }),

    'conditional-b': Flags.string({
      description: 'A conditionally depended-on flag.',
    }),

    'conditional-trigger': Flags.string({
      description: 'A trigger for a conditional dependency.',
      options: ['met', 'unmet'],
    }),
    // dependsOn relationship
    'dependent-flag': Flags.string({
      dependsOn: ['required-flag'],
      description: 'This flag depends on required-flag.',
    }),

    // exactlyOne relationship
    'exactly-one-a': Flags.boolean({
      description: 'Exactly one of exclusive-a or exclusive-b must be present.',
      exactlyOne: ['exactly-one-b', 'exactly-one-a'],
    }),
    'exactly-one-b': Flags.boolean({
      description: 'Exactly one of exclusive-a or exclusive-b must be present.',
      exactlyOne: ['exactly-one-a', 'exactly-one-b'],
    }),

    // exclusive relationship
    'exclusive-a': Flags.boolean({
      description: 'Cannot be used with exclusive-b.',
      exclusive: ['exclusive-b'],
    }),
    'exclusive-b': Flags.boolean({
      description: 'Cannot be used with exclusive-a.',
      exclusive: ['exclusive-a'],
    }),
    // 'all' relationship
    'rel-all-a': Flags.string({
      description: 'Depends on rel-all-b AND rel-all-c.',
      relationships: [{flags: ['rel-all-b', 'rel-all-c'], type: 'all'}],
    }),

    'rel-all-b': Flags.string({description: 'Part of an ALL relationship.'}),
    'rel-all-c': Flags.string({description: 'Part of an ALL relationship.'}),
    // 'none' relationship
    'rel-none-a': Flags.string({
      description: 'Exclusive of rel-none-b AND rel-none-c.',
      relationships: [{flags: ['rel-none-b', 'rel-none-c'], type: 'none'}],
    }),

    'rel-none-b': Flags.string({description: 'Part of a NONE relationship.'}),
    'rel-none-c': Flags.string({description: 'Part of a NONE relationship.'}),

    // 'some' relationship
    'rel-some-a': Flags.string({
      description: 'Depends on rel-some-b OR rel-some-c.',
      relationships: [{flags: ['rel-some-b', 'rel-some-c'], type: 'some'}],
    }),

    'rel-some-b': Flags.string({description: 'Part of a SOME relationship.'}),
    'rel-some-c': Flags.string({description: 'Part of a SOME relationship.'}),

    // Simple required flag
    'required-flag': Flags.string({
      description: 'A simple required flag.',
      required: true,
    }),

    'simple-options-flag': Flags.string({
      default: async () => 'option1',
      description: 'A simple flag with options.',
      options: ['option1', 'option2', 'option3'],
    }),
    'simple-string-flag': Flags.string({
      default: 'default',
      description: 'A simple flag.',
    }),
  }

  async customPromptingLogic(
    promptGen: AsyncGenerator<PromptGenerator.FlagPromptInfo>,
  ): Promise<Record<string, unknown>> {
    const answers: Record<string, unknown> = {}

    for await (const flagInfo of promptGen) {
      const prompt = this.flagToPrompt(flagInfo) // Convert to prompting library format
      const answer = await prompt?.() // Show prompt to user

      answers[flagInfo.name] = answer
      // Send answer back to generator to update dependency graph
      await promptGen.next(answer)
    }

    return answers
  }

  async run(): Promise<void> {
    const graph = PromptGenerator.buildFlagDependencyGraph(World.flags)
    console.log(inspect(graph, {depth: null}))
    const promptGen = PromptGenerator.promptGenerator(graph, {})

    const answers = await this.customPromptingLogic(promptGen)

    console.log(answers)
  }

  private flagToPrompt(flagInfo: PromptGenerator.FlagPromptInfo) {
    const {definition, name} = flagInfo

    // Example mapping for @inquirer/prompts
    if (definition.type === 'boolean') {
      return async () =>
        confirm({
          // @ts-expect-error - TODO: fix this
          default: typeof definition.default === 'function' ? await definition.default({}) : definition.default,
          message: `${name} (${definition.description})?`,
        })
    }

    if (definition.type === 'option') {
      if (definition.options) {
        return async () =>
          select({
            choices: definition.options?.map((value) => ({name: value, value})) ?? [],
            default: typeof definition.default === 'function' ? await definition.default({}) : definition.default,
            message: `${name} (${definition.description})?`,
          })
      }

      return async () =>
        input({
          default: typeof definition.default === 'function' ? await definition.default({}) : definition.default,
          message: `${name} (${definition.description})?`,
          // validate: async (input) => {
          //   try {
          //     await definition.parse(input, /* context */, /* opts */)
          //     return true
          //   } catch (error) {
          //     return error.message
          //   }
          // }
        })
    }
  }
}

@mdonnalley mdonnalley marked this pull request as draft July 21, 2025 21:10
@AllanOricil
Copy link
Contributor

#1507

Besides the benefits outlined in the ticket for the new constraints api, I believe that the schema created with the DSL to model the command constraints would help a lot here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants