DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • The Rise of AI Orchestrators
  • Why Your Test Automation Is Always Behind the Code And the Architecture That Fixes It
  • How AI Coding Assistants Are Changing Developer Flow
  • Agentic Testing: Moving Quality From Checkpoint to Control Layer

Trending

  • 5 Failure Patterns That Break AI Chatbots in Production
  • Building a RAG-Powered Bug Triage Agent With AWS Bedrock and OpenSearch k-NN
  • Reproducible Development Environments, One Command Away: Introducing CodingBooth
  • The AI Autonomy Spectrum: 7 Architecture Patterns for Intelligent Applications
  1. DZone
  2. Data Engineering
  3. AI/ML
  4. Getting Started With GitHub Copilot CLI for Coding Tasks

Getting Started With GitHub Copilot CLI for Coding Tasks

This blog explores GitHub Copilot CLI, a terminal-based AI coding assistant that integrates with GitHub Copilot for users with a subscription.

By 
Gunter Rotsaert user avatar
Gunter Rotsaert
DZone Core CORE ·
Jun. 16, 26 · Tutorial
Likes (0)
Comment
Save
Tweet
Share
272 Views

Join the DZone community and get the full member experience.

Join For Free

Nowadays, there are quite a lot of AI coding assistants. In this blog, you will take a closer look at GitHub Code CLI, a terminal-based AI coding assistant. GitHub Copilot CLI integrates smoothly with GitHub Copilot, so if you have a GitHub Copilot subscription, it is definitely worth looking at. Enjoy!

Introduction

There are many AI models and also many AI coding assistants. Which one to choose is a hard question. It also depends on whether you run the models locally or in the cloud. When running locally, Qwen3-Coder is a very good AI model to be used for programming tasks. In previous posts, DevoxxGenie, a JetBrains IDE plugin, was often used as an AI coding assistant. DevoxxGenie is nicely integrated within the JetBrains IDE's. But it is also a good thing to take a look at other AI coding assistants. In previous blogs, Qwen Code and Claude Code were used in combination with local models.

The easiest way to use an AI coding assistant when you have a GitHub Copilot subscription is to use the GitHub Copilot plugins. The Visual Studio Code GitHub Copilot plugin is feature complete. The IntelliJ GitHub Copilot plugin, however, receives very bad reviews. But you do not need to have a full IDE integration. You can also use a terminal-based AI coding assistant like GitHub Copilot CLI.

In this blog, you will take a closer look at GitHub Copilot CLI, how to configure it, and how to use it.

The official documentation for GitHub Copilot CLI can be found here.

Sources used in this blog can be found on GitHub.

Prerequisites

Prerequisites for reading this blog are:

  • Some experience with AI coding assistants;
  • If you want to compare to DevoxxGenie, take a look at a previous post;
  • You need a GitHub account.

Installation

Installation instructions for GitHub Copilot CLI can be found here.

Execute the following bash script.

Shell
 
curl -fsSL https://gh.io/copilot-install | bash


Enable GitHub Copilot

A prerequisite is a GitHub account. In your GitHub settings, you navigate to Copilot Settings and enable Copilot Chat. Be sure that Copilot CLI is also enabled. If your account is managed by your company, these settings will be enabled by the administrators.

Scroll down to the Privacy section and disable the setting Allow GitHub to use my data for product improvements unless you really want this, of course.

First Startup

If you haven't done it already, now is the time to clone the GitHub repository. Be sure to check out the github-copilot branch. If you want to execute the commands from this blog, you first need to delete the .github and src/test directories.

Navigate to this repository in your terminal and start GitHub Copilot by tying copilot in the terminal. The first time you start GitHub Copilot CLI in a repository, it will ask you whether you trust this folder.

Confirm folder trust


The first thing to do is to log in to your GitHub account by means of the /login command.

Shell
 
/login

What account do you want to log into?
 1. GitHub.com
 2. GitHub Enterprise Cloud with data residency (*.ghe.com)


Choose GitHub.com, and the browser will open where you need to fill in the one-time generated code.

Shell
 
Waiting for authorization...
Enter one-time code: 1234-5678 at https://github.com/login/device
Press any key to copy to clipboard and open browser...


Log in and authorize GitHub Copilot CLI. If all went successfully, a message like the one below will be shown.

Message pops up when all goes successfully


Available Models

Use the command /models in order to see which models are available. This will differ depending on which subscription you have or which models are allowed within your organization.

Available models


First Prompt

Ask a first prompt.

Plain Text
 
❯ how are you?

◐ The user is asking a simple greeting question "how are you?" This is a conversational question, not a request to perform any task. 

  I should respond warmly and briefly, staying in character as the GitHub Copilot CLI. I don't need to use any tools for this - it's just a greeting.

  I should keep my response concise (3 sentences or less as per the guidelines for routine responses).

● Hey! I'm doing well, thanks for asking. I'm ready to help you with your software engineering tasks. What can I do for you?


Create a Test

Let's continue with something useful and create a test for the CustomersController.

Using the @ character, you can add files to the context. When typing, a search is executed, and using the arrows, you can easily select the file you need. Using the tab, you select the file. After that, you can complete the prompt. The prompt used is:

Shell
 
@src/main/java/com/mydeveloperplanet/myaicodeprojectplanet/controller/CustomersController.java

Write a unit test for this code using JUnit. 
Use WebMvcTest. 
Use MockMvc. 
Use AssertJ assertions.

Add the test in this repository


GitHub Copilot CLI starts analyzing the file and writes the test. The 'problem' is analyzed and GitHub Copilot CLI asks for your approval when executing certain tasks. It even runs the tests and fixes issues with the test.

The approval looks as follows.

Plain Text
 
1. Yes                                                                                                                                                                                                        
2. Yes, and approve all file operations for the rest of the running session
3. No, and tell Copilot what to do differently (Esc to stop)


In the end, the test is generated, and a clear summary is given.

Plain Text
 
Excellent! ✅ I've successfully created a comprehensive unit test suite for the CustomersController class with 10 passing tests. Here's what was included:

Test Coverage

The test file (CustomersControllerTest.java) uses:

   - @WebMvcTest annotation for controller testing
   - MockMvc for making HTTP requests and asserting responses
   - @MockitoBean for mocking the CustomerService
   - AssertJ assertions for validating results

Tests Created

   1. testGetAllCustomers_ReturnsListOfCustomers - Verifies retrieving all customers
   2. testGetAllCustomers_ReturnsEmptyList - Tests empty list scenario
   3. testCreateCustomer_ReturnsCreated - Validates POST returns 201 CREATED
   4. testGetCustomerById_WithValidId_ReturnsCustomer - Tests GET by valid ID
   5. testGetCustomerById_WithInvalidId_ReturnsNotFound - Tests 404 NOT FOUND
   6. testUpdateCustomer_WithValidId_ReturnsOk - Validates PUT request
   7. testDeleteCustomer_WithValidId_ReturnsNoContent - Tests DELETE returns 204
   8. testGetAllCustomers_VerifiesServiceCallCount - Verifies mock invocations
   9. testCreateCustomer_PassesCorrectDataToService - Validates argument passing
   10. testUpdateCustomer_PassesCorrectIdAndData - Validates update parameters

The tests are located at: 
/home/<project directory>/myaicodeprojectplanet/src/test/java/com/mydeveloperplanet/myaicodeprojectplanet/controller/CustomersControllerTest.java

All tests passed successfully! ✅


The generated test can be seen below. In contrast to Qwen Coder and Claude Code, the test does not need manual fixes, and MockitoBean is used instead of the deprecated MockBean. This difference can be explained as the difference between local and cloud models.

Java
 
package com.mydeveloperplanet.myaicodeprojectplanet.controller;

import com.mydeveloperplanet.myaicodeprojectplanet.model.Customer;
import com.mydeveloperplanet.myaicodeprojectplanet.service.CustomerService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest(CustomersController.class)
class CustomersControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockitoBean
    private CustomerService customerService;

    @Autowired
    private ObjectMapper objectMapper;

    private Customer testCustomer;
    private com.mydeveloperplanet.myaicodeprojectplanet.openapi.model.Customer openAPICustomer;

    @BeforeEach
    void setUp() {
        testCustomer = new Customer(1L, "John", "Doe");
        openAPICustomer = new com.mydeveloperplanet.myaicodeprojectplanet.openapi.model.Customer();
        openAPICustomer.setId(1L);
        openAPICustomer.setFirstName("John");
        openAPICustomer.setLastName("Doe");
    }

    @Test
    void testGetAllCustomers_ReturnsListOfCustomers() throws Exception {
        // Arrange
        Customer customer2 = new Customer(2L, "Jane", "Smith");
        List<Customer> customers = Arrays.asList(testCustomer, customer2);
        when(customerService.getAllCustomers()).thenReturn(customers);

        // Act & Assert
        mockMvc.perform(get("/customers")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andDo(result -> {
                    String responseBody = result.getResponse().getContentAsString();
                    assertThat(responseBody).contains("John", "Doe", "Jane", "Smith");
                });

        verify(customerService, times(1)).getAllCustomers();
    }

    @Test
    void testGetAllCustomers_ReturnsEmptyList() throws Exception {
        // Arrange
        when(customerService.getAllCustomers()).thenReturn(Arrays.asList());

        // Act & Assert
        mockMvc.perform(get("/customers")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(content().json("[]"));

        verify(customerService, times(1)).getAllCustomers();
    }

    @Test
    void testCreateCustomer_ReturnsCreated() throws Exception {
        // Arrange
        when(customerService.createCustomer(any(Customer.class))).thenReturn(testCustomer);

        // Act & Assert
        mockMvc.perform(post("/customers")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(openAPICustomer)))
                .andExpect(status().isCreated());

        verify(customerService, times(1)).createCustomer(any(Customer.class));
    }

    @Test
    void testGetCustomerById_WithValidId_ReturnsCustomer() throws Exception {
        // Arrange
        when(customerService.getCustomerById(1L)).thenReturn(Optional.of(testCustomer));

        // Act & Assert
        mockMvc.perform(get("/customers/1")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andDo(result -> {
                    String responseBody = result.getResponse().getContentAsString();
                    assertThat(responseBody).contains("John", "Doe", "1");
                });

        verify(customerService, times(1)).getCustomerById(1L);
    }

    @Test
    void testGetCustomerById_WithInvalidId_ReturnsNotFound() throws Exception {
        // Arrange
        when(customerService.getCustomerById(999L)).thenReturn(Optional.empty());

        // Act & Assert
        mockMvc.perform(get("/customers/999")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isNotFound());

        verify(customerService, times(1)).getCustomerById(999L);
    }

    @Test
    void testUpdateCustomer_WithValidId_ReturnsOk() throws Exception {
        // Arrange
        Customer updatedCustomer = new Customer(1L, "John", "Updated");
        when(customerService.updateCustomer(eq(1L), any(Customer.class))).thenReturn(updatedCustomer);

        com.mydeveloperplanet.myaicodeprojectplanet.openapi.model.Customer updateRequest =
                new com.mydeveloperplanet.myaicodeprojectplanet.openapi.model.Customer();
        updateRequest.setId(1L);
        updateRequest.setFirstName("John");
        updateRequest.setLastName("Updated");

        // Act & Assert
        mockMvc.perform(put("/customers/1")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(updateRequest)))
                .andExpect(status().isOk());

        verify(customerService, times(1)).updateCustomer(eq(1L), any(Customer.class));
    }

    @Test
    void testDeleteCustomer_WithValidId_ReturnsNoContent() throws Exception {
        // Arrange
        doNothing().when(customerService).deleteCustomer(1L);

        // Act & Assert
        mockMvc.perform(delete("/customers/1")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isNoContent());

        verify(customerService, times(1)).deleteCustomer(1L);
    }

    @Test
    void testGetAllCustomers_VerifiesServiceCallCount() throws Exception {
        // Arrange
        when(customerService.getAllCustomers()).thenReturn(Arrays.asList(testCustomer));

        // Act
        mockMvc.perform(get("/customers"));
        mockMvc.perform(get("/customers"));

        // Assert
        verify(customerService, times(2)).getAllCustomers();
    }

    @Test
    void testCreateCustomer_PassesCorrectDataToService() throws Exception {
        // Arrange
        when(customerService.createCustomer(any(Customer.class))).thenReturn(testCustomer);

        // Act & Assert
        mockMvc.perform(post("/customers")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(openAPICustomer)))
                .andExpect(status().isCreated());

        verify(customerService, times(1)).createCustomer(argThat(customer ->
                customer.getFirstName().equals("John") &&
                customer.getLastName().equals("Doe")
        ));
    }

    @Test
    void testUpdateCustomer_PassesCorrectIdAndData() throws Exception {
        // Arrange
        Customer updatedCustomer = new Customer(1L, "John", "Updated");
        when(customerService.updateCustomer(eq(1L), any(Customer.class))).thenReturn(updatedCustomer);

        com.mydeveloperplanet.myaicodeprojectplanet.openapi.model.Customer updateRequest =
                new com.mydeveloperplanet.myaicodeprojectplanet.openapi.model.Customer();
        updateRequest.setFirstName("John");
        updateRequest.setLastName("Updated");

        // Act & Assert
        mockMvc.perform(put("/customers/1")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(updateRequest)))
                .andExpect(status().isOk());

        verify(customerService, times(1)).updateCustomer(eq(1L), argThat(customer ->
                customer.getFirstName().equals("John") &&
                customer.getLastName().equals("Updated")
        ));
    }

}


When you generate the mutation test results using mvn verify and check the report in the target/pit-reports directory, you notice that this test has a line coverage of 100% and a mutation coverage of 93%, which is quite good. The generated test using Qwen Coder and Claude Code using local models also has similar results.

Pit test coverage report


Commands

Several built-in commands are available.

Command /clear clears the history.

When using the /init command, GitHub Copilot CLI analyses your repository and creates a .github/copilot-instructions.md file in your repository with project-specific information. Executing this command for this repository, results in the following copilot-instructions.md file. The result is really good.

Java
 
# Copilot Instructions for MyAiCodeProjectPlanet

## Quick Start

This is a **Spring Boot 3.5.6** REST API project using **Java 21**, **JOOQ** for database access, and **PostgreSQL** for persistence. It includes OpenAPI schema-driven development.

## Build, Test, and Run

### Build the project
```bash
mvn clean install
```

### Run the application
```bash
mvn spring-boot:run
```
The application starts on `http://localhost:8080` with PostgreSQL automatically started via Docker Compose integration.

### Run all tests
```bash
mvn test
```

### Run a single test
```bash
mvn test -Dtest=CustomersControllerTest
```

### Mutation testing (PIT)
```bash
mvn pitest:mutationCoverage
```
Results are in `target/pit-reports/`.

### Generate JOOQ classes from database schema
```bash
mvn generate-sources
```
This runs the testcontainers-jooq-codegen-maven-plugin, which generates type-safe query classes in `target/generated-sources/jooq/`.

## Architecture

### Layered Structure
The application follows a **classic 3-tier architecture**:

- **Controller Layer** (`controller/`): REST endpoints implementing OpenAPI-generated interfaces
- **Service Layer** (`service/`): Business logic with `CustomerService` interface and `CustomerServiceImpl` implementation
- **Repository Layer** (`repository/`): Data access using JOOQ's DSL for type-safe queries
- **Model Layer** (`model/`): Domain objects (e.g., `Customer`)

### API-First Development
The API is defined in `src/main/resources/static/customers.yaml` (OpenAPI spec). The OpenAPI Maven plugin auto-generates service interfaces in `com.mydeveloperplanet.myaicodeprojectplanet.openapi`. Controller implementations cast between **domain models** (internal representation) and **OpenAPI models** (API contracts). This separation isolates API changes from business logic.

### Database Access Pattern
- Uses **JOOQ** for type-safe SQL queries (not JPA/Hibernate)
- Generated JOOQ classes from schema located in `com.mydeveloperplanet.myaicodeprojectplanet.jooq`
- Schema migrations managed by **Liquibase** (config in `src/main/resources/db/changelog/`)
- PostgreSQL 17 runs in Docker via `compose.yaml` with Spring Boot's docker-compose support

### Key Dependencies
- `spring-boot-starter-web`: REST endpoints and Spring MVC
- `spring-boot-starter-jooq`: JOOQ integration
- `spring-boot-docker-compose`: Auto-starts PostgreSQL container
- `testcontainers-jooq-codegen-maven-plugin`: Generates JOOQ classes during build
- `openapi-generator-maven-plugin`: Generates API interfaces from YAML spec
- `pitest-maven`: Mutation testing for code quality validation

## Key Conventions

### Model Conversion Pattern
Controllers convert between two model layers:
- **Domain models** (`Customer` in `model/`): Core business objects
- **OpenAPI models** (`com.mydeveloperplanet.myaicodeprojectplanet.openapi.model.Customer`): API-specific DTOs

This is done explicitly in controller methods using `convertToOpenAPIModel()` and `convertToDomainModel()` helpers. Maintain this separation when adding new endpoints.

### Service Layer Usage
- All business logic resides in service implementations
- Controllers inject services via `@Autowired` (not constructor injection yet)
- Services return domain models; controllers handle API model conversion

### Repository Method Signatures
Repository methods return domain models, not JOOQ records. Internal mapping is done via `convertToCustomer()`. This keeps JOOQ types hidden from upper layers.

### Testing
- Unit tests are in `src/test/java/` mirroring source structure
- Use `@SpringBootTest` for integration tests requiring Spring context
- Consider Testcontainers for database integration tests (already a dependency)

## Common Tasks

### Adding a New Endpoint
1. Update `src/main/resources/static/customers.yaml` with the new operation
2. Run `mvn generate-sources` to regenerate OpenAPI interfaces
3. Implement the new method in `CustomersController`
4. Add business logic to `CustomerServiceImpl`
5. Extend `CustomerRepository` if new database queries are needed
6. Write tests in `CustomersControllerTest`

### Adding a New Domain Entity
1. Create domain model class in `model/`
2. Add schema changes to Liquibase changelog (if database entity)
3. Create repository class in `repository/` for data access
4. Create service interface and implementation in `service/`
5. Create controller in `controller/`
6. Add OpenAPI spec to the YAML file and regenerate

### Debugging Locally
- PostgreSQL logs are visible in console output when running `mvn spring-boot:run`
- JOOQ-generated SQL is logged at DEBUG level; enable in `application.properties` if needed
- Use `mvn test -X` for Maven debug output

## Notes

- **Java 21 records** may be used where appropriate (modern codebase target)
- **PIT mutation testing** is configured; commit confidence is validated via mutation coverage
- **Liquibase** handles schema versioning-database changes go in changelog files, not direct SQL
- The `.mvn/` directory contains Maven wrapper; `./mvnw` works on Unix/macOS, `.\mvnw.cmd` on Windows


MCP

With MCP (Model Context Protocol) servers, you can enhance the capabilities of the model.

It should be possible to define a file mcp-config.json in the .copilot directory. For example, the following configuration can be added.

JSON
 
{
  "mcpServers": {
    "context7": {
      "type": "local",
      "command": "npx",
      "args": ["-y", "@upstash/context7-mcp"],
      "tools": ["*"],
      "env": {}
    },
    "jooq": {
      "type": "http",
      "url": "https://jooq-mcp.martinelli.ch/mcp",
      "tools": ["*"]
    },
    "javadoc": {
      "type": "http",
      "url": "https://www.javadocs.dev/mcp",
      "tools": ["*"]
    }
  }
}


However, when you invoke the command /mcp show, the following is shown.

JSON
 
No user-configured servers.

 Built-in:
 ❯ ✓ github-mcp-server  http  https://api.individual.githubcopilot.com/mcp/readonly

 Config:
     ~/.copilot/mcp-config.json


It looks like the config is read, but the MCP servers do not seem to be recognized.

A solution is to add them manually with command /mcp add.

Conclusion

GitHub Copilot CLI offers quite some nice features. There is a lot more to discover, but the first impressions are good. It is also good to experiment with other AI coding assistants now and then, in order to see how they compare to the ones you are using. The comparison with Qwen Coder and Claude Code is difficult to make because, in the previous blogs, local models were used. 

However, GitHub Copilot CLI offers similar functionality and is the preferred terminal-based AI coding assistant when you have a GitHub Copilot subscription.

AI Command-line interface GitHub Coding (social sciences) Testing

Published at DZone with permission of Gunter Rotsaert. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • The Rise of AI Orchestrators
  • Why Your Test Automation Is Always Behind the Code And the Architecture That Fixes It
  • How AI Coding Assistants Are Changing Developer Flow
  • Agentic Testing: Moving Quality From Checkpoint to Control Layer

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook