Skip to content

Conversation

@agusayerza
Copy link
Contributor

@agusayerza agusayerza commented Dec 17, 2025

This PR improves the CLI's user experience by introducing an interactive mode for the create, dryrun, deploy, and init commands.

When a user runs one of these commands without providing all the required arguments, the CLI will now launch an interactive prompt to guide them through the missing options. This makes the CLI easier to use, as it no longer requires users to memorize all command-line arguments.

The interactive prompts are built using inquirer to provide a user-friendly selection list for known values (like function types or environments).

The interactive mode is enabled by default and can be disabled with the --no-interactive flag or by setting the CI environment variable, ensuring backward compatibility and scriptability.

Fixes NAN-4237

Testing:

  1. Run npm install to add the new inquirer dependency.
  2. Run nango create without any arguments. Verify that you are prompted to select a function type, integration, and provide a name.
  3. Run nango dryrun without any arguments. Verify that you are prompted to select an environment, a function, and a connection.
  4. Run nango deploy without an environment. Verify that you are prompted to select one.
  5. Run nango init without a path. Verify that you are prompted to enter one.
  6. Run any of the above commands with the --no-interactive flag to confirm that the prompts are skipped and the command fails due to missing arguments.

A shared Ensure utility now orchestrates the interactive prompts, translating missing-argument errors into actionable guidance while preserving non-interactive execution when required.

Affected Areas

• packages/cli/lib/index.ts
• packages/cli/lib/services/interactive.service.ts
• packages/cli/lib/services/ensure.service.ts
• packages/cli/lib/services/function-create.service.ts
• packages/cli/lib/types.ts
• packages/cli/lib/services/sdk.ts
• packages/cli/lib/utils/errors.ts
• packages/cli/tsconfig.json
• packages/cli/package.json
• docs/reference/cli.mdx
• package-lock.json


This summary was automatically generated by @propel-code-bot

@agusayerza agusayerza requested a review from a team December 17, 2025 19:33
@agusayerza agusayerza self-assigned this Dec 17, 2025
@linear
Copy link

linear bot commented Dec 17, 2025

@my-senior-dev-pr-review
Copy link

my-senior-dev-pr-review bot commented Dec 17, 2025

🤖 My Senior Dev — Analysis Complete

👤 For @agusayerza

📁 Expert in packages/ (16 edits) • ⚡ 3rd PR this month

View your contributor analytics →


📊 13 files reviewed • 10 need attention

⚠️ Needs Attention:

  • packages/cli/lib/index.ts — Changes to command execution and interactive behavior raise security and logic concerns.
  • packages/cli/lib/services/ensure.service.ts — This service manages user prompt interactions and has potential security risks if user inputs are not validated.
  • +2 more concerns...

🚀 Open Interactive Review →

The full interface unlocks features not available in GitHub:

  • 💬 AI Chat — Ask questions on any file, get context-aware answers
  • 🔍 Smart Hovers — See symbol definitions and usage without leaving the diff
  • 📚 Code Archeology — Understand how files evolved over time (/archeology)
  • 🎯 Learning Insights — See how this PR compares to similar changes

💬 Chat here: @my-senior-dev explain this change — or try @chaos-monkey @security-auditor @optimizer @skeptic @junior-dev

📖 View all 12 personas & slash commands

You can interact with me by mentioning @my-senior-dev in any comment:

In PR comments or on any line of code:

  • Ask questions about the code or PR
  • Request explanations of specific changes
  • Get suggestions for improvements

Slash commands:

  • /help — Show all available commands
  • /archeology — See the history and evolution of changed files
  • /profile — Performance analysis and suggestions
  • /expertise — Find who knows this code best
  • /personas — List all available AI personas

AI Personas (mention to get their perspective):

Persona Focus
@chaos-monkey 🐵 Edge cases & failure scenarios
@skeptic 🤨 Challenge assumptions
@optimizer Performance & efficiency
@security-auditor 🔒 Security vulnerabilities
@accessibility-advocate Inclusive design
@junior-dev 🌱 Simple explanations
@tech-debt-collector 💳 Code quality & shortcuts
@ux-champion 🎨 User experience
@devops-engineer 🚀 Deployment & scaling
@documentation-nazi 📚 Documentation gaps
@legacy-whisperer 🏛️ Working with existing code
@test-driven-purist Testing & TDD

For the best experience, view this PR on myseniordev.com — includes AI chat, file annotations, and interactive reviews.

console.error(chalk.red(parsing.error));
}
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it make sense to pass the integrations as argument and move the the parsing/loading outside of the interactive logic? Same for fetching the connections below?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I moved the parsing logic out and just pass the list to the prompt function now

type: 'rawlist',
name: 'env',
message: 'Which environment do you want to use?',
choices: ['dev', 'prod', new inquirer.Separator(), OTHER_CHOICE]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not fetching the environments from the API?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked the API and there is no endpoint to list environments yet, maybe I missed it? I talked with Bastien of this being a fast-follow-up

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just checked the code, endpoint exists but not documented or exposed on the SDK. My bad. I still think this is an improvement to be done as a fast-follow-up

cmd.option('--auto-confirm', 'Auto confirm yes to all prompts.', false);
cmd.option('--debug', 'Run cli in debug mode, outputting verbose logs.', false);
// Default to true so that interactive mode is enabled by default
cmd.option('--no-interactive', 'Disable interactive prompts for missing arguments.', true);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure I understand why --no-interactive is true by default if we want the interactive mode enabled by default

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--no prefix on commander sets the flag to false when you pass it instead of true. Added a better description to the comment to explain that weird behaviour

.action(async function (this: Command) {
const { debug, ai, copy } = this.opts<GlobalOptions & { ai: string[]; copy: boolean }>();
const { debug, ai, copy, interactive } = this.opts<GlobalOptions & { ai: string[]; copy: boolean }>();
let [projectPath] = this.args;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is projecPath being used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. It's being used now to resolve the absolute path, missed it

if (!functionType || !integration || !name) {
console.error(chalk.red('Error: Missing required arguments. Use --sync, --action, or --on-event, and provide an integration and a name.'));
this.help();
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like extracting this logic in its own file/class with functions and early returns will reduce the branching, make it easier to read and more testable as well as reusable in the different commands. Something like that:

export class Ensure {
    constructor(private readonly interactive: boolean)

    private async ensure<T>(
      currentValue: T | undefined,
      promptFn: () => Promise<T>,
      errorMessage: string
  ): Promise<T> {
      if (currentValue) return currentValue;
      if (!interactive) throw new MissingArgumentError(errorMessage);
      try {
      return await promptFn();
      catch {
           // show error and process exit
      }
  }

  public async functionType(
      sync: boolean,
      action: boolean,
      onEvent: boolean,
  ): Promise<FunctionType> {
      if (sync) return 'sync';
      if (action) return 'action';
      if (onEvent) return 'on-event';

      if (!this.interactive) {
          throw new MissingArgumentError('Must specify --sync, --action, or --on-event');
      }

      return await promptForFunctionType();
  }

  public async integration(
      current: string | undefined,
      context: { fullPath?: string; isNangoFolder: boolean; isZeroYaml:
  boolean; debug?: boolean }
  ): Promise<string> {
      return ensure(
          current,
          () => promptForIntegrationName(context),
          'Integration name is required'
      );
  }

  public async functionName(
      current: string | undefined,
      functionType: FunctionType,
  ): Promise<string> {
      return ensure(
          current,
          () => promptForFunctionName(functionType),
          'Function name is required'
      );
  }

  export async function environment(
      current: string | undefined,
      interactive: boolean
  ): Promise<string> {
      return ensure(
          current,
          interactive,
          () => promptForEnvironment(),
          'Environment is required'
      );
  }

  public async function(
      current: string | undefined,
      availableFunctions: { name: string; type: string }[]
  ): Promise<string> {
      return ensure(
          current,
          () => promptForFunctionToRun(availableFunctions),
          'Function name is required'
      );
  }

  public async connection(
      current: string | undefined,
      environment: string
  ): Promise<string> {
      return ensure(
          current,
          () => promptForConnection(environment),
          'Connection ID is required'
      );
  }

  public async projectPath(
      current: string | undefined,
  ): Promise<string> {
      return ensure(
          current,
          () => promptForProjectPath(),
          'Project path is required'
      );
  }

so it can be used like this

const ensure = new Ensure(interactive)
const functionType = await ensure.functionType(sync, action,
  onEvent);
integration = await ensure.integration(integration, { ... });
name = await ensure.functionName(name, functionType);

wdyt?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great suggestion, implemented it

connectionId = await promptForConnection(environment);
}
} catch (err: any) {
console.error(chalk.red(err.isTtyError ? "Prompt couldn't be rendered in the current environment" : err.message));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add some info about how users could fix the issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a hint to use --no-interactive if the prompt fails (I would expect this to happen in CI/non-TTY).

"glob": "11.1.0",
"import-meta-resolve": "4.1.0",
"jscodeshift": "17.3.0",
"inquirer": "^13.1.0",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we always pin to a specific version. We have all collectively been burned too many time by breaking changes (even in patch version)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, pinned it

{
"name": "nango",
"version": "0.69.20",
"version": "0.70.0",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on if this should be a minor or a patch? Sticking to strict semver its a minor, but open to change it if anyone thinks its a patch

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it a breaking change? aka: does it modify the behavior of existing commands? if yes, minor otherwise patch

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

version isn't managed by modifying the package.json directly. The release workflow takes care of it

@agusayerza agusayerza requested a review from a team December 18, 2025 18:39
@@ -0,0 +1,6 @@
export class MissingArgumentError extends Error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is also packages/cli/lib/utils/errors.ts. Can we unify them?

Copy link
Contributor Author

@agusayerza agusayerza Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed that file, will merge them


import { initAI } from './ai/init.js';
import { generate, getVersionOutput, tscWatch } from './cli.js';
import { MissingArgumentError } from './errors.js';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ooc: Why do we need to import .js files here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ESM modules require file extension on imports. Addtionally, my understanding is that typescript doesn't change file extensions when transpiling to javascript, so we need to reference the js files.

Might be wrong and there is another reason why this is done, but I think this is the case


// opts.interactive is true by default (from the option default), or false if --no-interactive is passed.
// We also disable it if we are in a CI environment.
opts.interactive = opts.interactive && !isCI;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When would this be used in a CI environment?

I'm not sure it's a great idea to have the completely opposite behavior of a flag in CI instead of outside of it. Would it make more sense to raise an error in CI environment and make sure people specify --no-interactive instead? This would be a breaking change, though, hence the question above :P

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some customers use the CLI today on CI envs to test and deploy nango. If we force them to adopt the --no-interactive flag we would break their CI pipelines. At the same time, we want interactive mode to be the default use case for people using the CLI on the terminal, so this gets both things done. A warning for CI envs that do not provide the --no-interactive flag might be in order though. Wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup I think a warning would be great.


```bash
# Command flag to auto-confirm all prompts (useful for CI).
# Note: Destructive changes (like removing a sync or renaming a model) requires confirmation, even when --auto-confirm is set. To bypass this restriction, the --allow-destructive flag can be passed to nango deploy.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Advisory

[Documentation] Fix subject-verb agreement: "Destructive changes ... requires" should be "Destructive changes ... require".

Context for Agents
[**Documentation**]

Fix subject-verb agreement: "Destructive changes ... requires" should be "Destructive changes ... require".

File: docs/reference/cli.mdx
Line: 106

Copy link
Collaborator

@TBonnin TBonnin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm. Just the package version and dependency pinning and the rest is good to go I think

"@types/commander": "2.12.5",
"@types/ejs": "3.1.5",
"@types/figlet": "1.5.6",
"@types/inquirer": "^9.0.7",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pin, pin, pin

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does interactive and ensure belongs to the services folder? You are gonna ask what's a service in the context of the CLI and to be honest I am not sure I have an good answer :/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just followed suit, but it doesn't make any sense. Thought of it as just where the main logic leaves, but it is a bit a weird naming scheme if you ask me

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you don't need to change anything here. Let's keep it in mind though when/if we refactor the CLI ;)

@agusayerza agusayerza force-pushed the agus/NAN-4237/interactive-cli branch from 0c471eb to abe63c9 Compare January 14, 2026 13:26
@agusayerza agusayerza added this pull request to the merge queue Jan 14, 2026
Merged via the queue into master with commit 22e17a6 Jan 14, 2026
24 checks passed
@agusayerza agusayerza deleted the agus/NAN-4237/interactive-cli branch January 14, 2026 20:55
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.

4 participants