feat(cli): introduce /init command for GEMINI.md creation (#4852)

Co-authored-by: matt korwel <matt.korwel@gmail.com>
This commit is contained in:
shamso-goog 2025-07-29 12:49:01 -04:00 committed by GitHub
parent 7356764a48
commit 80079cd2a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 198 additions and 1 deletions

View File

@ -20,6 +20,7 @@ import { editorCommand } from '../ui/commands/editorCommand.js';
import { extensionsCommand } from '../ui/commands/extensionsCommand.js';
import { helpCommand } from '../ui/commands/helpCommand.js';
import { ideCommand } from '../ui/commands/ideCommand.js';
import { initCommand } from '../ui/commands/initCommand.js';
import { mcpCommand } from '../ui/commands/mcpCommand.js';
import { memoryCommand } from '../ui/commands/memoryCommand.js';
import { privacyCommand } from '../ui/commands/privacyCommand.js';
@ -59,9 +60,10 @@ export class BuiltinCommandLoader implements ICommandLoader {
extensionsCommand,
helpCommand,
ideCommand(this.config),
initCommand,
mcpCommand,
memoryCommand,
privacyCommand,
mcpCommand,
quitCommand,
restoreCommand(this.config),
statsCommand,

View File

@ -0,0 +1,102 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import { initCommand } from './initCommand.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import { type CommandContext } from './types.js';
// Mock the 'fs' module
vi.mock('fs', () => ({
existsSync: vi.fn(),
writeFileSync: vi.fn(),
}));
describe('initCommand', () => {
let mockContext: CommandContext;
const targetDir = '/test/dir';
const geminiMdPath = path.join(targetDir, 'GEMINI.md');
beforeEach(() => {
// Create a fresh mock context for each test
mockContext = createMockCommandContext({
services: {
config: {
getTargetDir: () => targetDir,
},
},
});
});
afterEach(() => {
// Clear all mocks after each test
vi.clearAllMocks();
});
it('should inform the user if GEMINI.md already exists', async () => {
// Arrange: Simulate that the file exists
vi.mocked(fs.existsSync).mockReturnValue(true);
// Act: Run the command's action
const result = await initCommand.action!(mockContext, '');
// Assert: Check for the correct informational message
expect(result).toEqual({
type: 'message',
messageType: 'info',
content:
'A GEMINI.md file already exists in this directory. No changes were made.',
});
// Assert: Ensure no file was written
expect(fs.writeFileSync).not.toHaveBeenCalled();
});
it('should create GEMINI.md and submit a prompt if it does not exist', async () => {
// Arrange: Simulate that the file does not exist
vi.mocked(fs.existsSync).mockReturnValue(false);
// Act: Run the command's action
const result = await initCommand.action!(mockContext, '');
// Assert: Check that writeFileSync was called correctly
expect(fs.writeFileSync).toHaveBeenCalledWith(geminiMdPath, '', 'utf8');
// Assert: Check that an informational message was added to the UI
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: 'info',
text: 'Empty GEMINI.md created. Now analyzing the project to populate it.',
},
expect.any(Number),
);
// Assert: Check that the correct prompt is submitted
expect(result.type).toBe('submit_prompt');
expect(result.content).toContain(
'You are an AI agent that brings the power of Gemini',
);
});
it('should return an error if config is not available', async () => {
// Arrange: Create a context without config
const noConfigContext = createMockCommandContext();
if (noConfigContext.services) {
noConfigContext.services.config = null;
}
// Act: Run the command's action
const result = await initCommand.action!(noConfigContext, '');
// Assert: Check for the correct error message
expect(result).toEqual({
type: 'message',
messageType: 'error',
content: 'Configuration not available.',
});
});
});

View File

@ -0,0 +1,93 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import * as fs from 'fs';
import * as path from 'path';
import {
CommandContext,
SlashCommand,
SlashCommandActionReturn,
CommandKind,
} from './types.js';
export const initCommand: SlashCommand = {
name: 'init',
description: 'Analyzes the project and creates a tailored GEMINI.md file.',
kind: CommandKind.BUILT_IN,
action: async (
context: CommandContext,
_args: string,
): Promise<SlashCommandActionReturn> => {
if (!context.services.config) {
return {
type: 'message',
messageType: 'error',
content: 'Configuration not available.',
};
}
const targetDir = context.services.config.getTargetDir();
const geminiMdPath = path.join(targetDir, 'GEMINI.md');
if (fs.existsSync(geminiMdPath)) {
return {
type: 'message',
messageType: 'info',
content:
'A GEMINI.md file already exists in this directory. No changes were made.',
};
}
// Create an empty GEMINI.md file
fs.writeFileSync(geminiMdPath, '', 'utf8');
context.ui.addItem(
{
type: 'info',
text: 'Empty GEMINI.md created. Now analyzing the project to populate it.',
},
Date.now(),
);
return {
type: 'submit_prompt',
content: `
You are an AI agent that brings the power of Gemini directly into the terminal. Your task is to analyze the current directory and generate a comprehensive GEMINI.md file to be used as instructional context for future interactions.
**Analysis Process:**
1. **Initial Exploration:**
* Start by listing the files and directories to get a high-level overview of the structure.
* Read the README file (e.g., \`README.md\`, \`README.txt\`) if it exists. This is often the best place to start.
2. **Iterative Deep Dive (up to 10 files):**
* Based on your initial findings, select a few files that seem most important (e.g., configuration files, main source files, documentation).
* Read them. As you learn more, refine your understanding and decide which files to read next. You don't need to decide all 10 files at once. Let your discoveries guide your exploration.
3. **Identify Project Type:**
* **Code Project:** Look for clues like \`package.json\`, \`requirements.txt\`, \`pom.xml\`, \`go.mod\`, \`Cargo.toml\`, \`build.gradle\`, or a \`src\` directory. If you find them, this is likely a software project.
* **Non-Code Project:** If you don't find code-related files, this might be a directory for documentation, research papers, notes, or something else.
**GEMINI.md Content Generation:**
**For a Code Project:**
* **Project Overview:** Write a clear and concise summary of the project's purpose, main technologies, and architecture.
* **Building and Running:** Document the key commands for building, running, and testing the project. Infer these from the files you've read (e.g., \`scripts\` in \`package.json\`, \`Makefile\`, etc.). If you can't find explicit commands, provide a placeholder with a TODO.
* **Development Conventions:** Describe any coding styles, testing practices, or contribution guidelines you can infer from the codebase.
**For a Non-Code Project:**
* **Directory Overview:** Describe the purpose and contents of the directory. What is it for? What kind of information does it hold?
* **Key Files:** List the most important files and briefly explain what they contain.
* **Usage:** Explain how the contents of this directory are intended to be used.
**Final Output:**
Write the complete content to the \`GEMINI.md\` file. The output must be well-formatted Markdown.
`,
};
},
};