Nowadays, there are quite a lot 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!
1. Introduction
There are many AI models and also many AI coding assistants. Which one to choose is a hard question. It also depends 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, often was 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 at GitHub.
2. 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.
3. Installation
Installation instructions for GitHub Copilot CLI can be found here.
Execute the following bash script.
curl -fsSL https://gh.io/copilot-install | bash
4. Enable GitHub Copilot
A prerequisite is the need of 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.
5. First Startup
If you haven’t done it already, now is the time to clone the GitHub repository. Be sure to checkout the github-copilot branch. If you want to execute the commands from this blog, you first 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.

First thing to do, is to login to your GitHub account by means of the /login command.
/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.
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...
Login and authorize GitHub Copilot CLI. If all went successfully, a message like the one below will be shown.

6. 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.

7. First Prompt
Ask a first prompt.
❯ 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?
8. 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 tab, you select the file. After that, you can complete the prompt. The prompt used is:
@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.
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.
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 contrary 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.
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.

9. 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.
# 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
10. 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.
{
"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.
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.
11. 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.
Discover more from My Developer Planet
Subscribe to get the latest posts sent to your email.

Leave a Reply