mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 16:57:46 +00:00
# 🚀 Sync Gemini CLI v0.2.1 - Major Feature Update (#483)
This commit is contained in:
@@ -11,6 +11,8 @@ import { createMockCommandContext } from '../../test-utils/mockCommandContext.js
|
||||
import * as versionUtils from '../../utils/version.js';
|
||||
import { MessageType } from '../types.js';
|
||||
|
||||
import { IdeClient } from '../../../../core/src/ide/ide-client.js';
|
||||
|
||||
vi.mock('../../utils/version.js', () => ({
|
||||
getCliVersion: vi.fn(),
|
||||
}));
|
||||
@@ -25,6 +27,8 @@ describe('aboutCommand', () => {
|
||||
services: {
|
||||
config: {
|
||||
getModel: vi.fn(),
|
||||
getIdeClient: vi.fn(),
|
||||
getIdeMode: vi.fn().mockReturnValue(true),
|
||||
},
|
||||
settings: {
|
||||
merged: {
|
||||
@@ -41,10 +45,13 @@ describe('aboutCommand', () => {
|
||||
vi.spyOn(mockContext.services.config!, 'getModel').mockReturnValue(
|
||||
'test-model',
|
||||
);
|
||||
process.env.GOOGLE_CLOUD_PROJECT = 'test-gcp-project';
|
||||
process.env['GOOGLE_CLOUD_PROJECT'] = 'test-gcp-project';
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'test-os',
|
||||
});
|
||||
vi.spyOn(mockContext.services.config!, 'getIdeClient').mockReturnValue({
|
||||
getDetectedIdeDisplayName: vi.fn().mockReturnValue('test-ide'),
|
||||
} as Partial<IdeClient> as IdeClient);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -62,7 +69,7 @@ describe('aboutCommand', () => {
|
||||
});
|
||||
|
||||
it('should call addItem with all version info', async () => {
|
||||
process.env.SANDBOX = '';
|
||||
process.env['SANDBOX'] = '';
|
||||
if (!aboutCommand.action) {
|
||||
throw new Error('The about command must have an action.');
|
||||
}
|
||||
@@ -78,13 +85,14 @@ describe('aboutCommand', () => {
|
||||
modelVersion: 'test-model',
|
||||
selectedAuthType: 'test-auth',
|
||||
gcpProject: 'test-gcp-project',
|
||||
ideClient: 'test-ide',
|
||||
},
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
it('should show the correct sandbox environment variable', async () => {
|
||||
process.env.SANDBOX = 'gemini-sandbox';
|
||||
process.env['SANDBOX'] = 'gemini-sandbox';
|
||||
if (!aboutCommand.action) {
|
||||
throw new Error('The about command must have an action.');
|
||||
}
|
||||
@@ -100,8 +108,8 @@ describe('aboutCommand', () => {
|
||||
});
|
||||
|
||||
it('should show sandbox-exec profile when applicable', async () => {
|
||||
process.env.SANDBOX = 'sandbox-exec';
|
||||
process.env.SEATBELT_PROFILE = 'test-profile';
|
||||
process.env['SANDBOX'] = 'sandbox-exec';
|
||||
process.env['SEATBELT_PROFILE'] = 'test-profile';
|
||||
if (!aboutCommand.action) {
|
||||
throw new Error('The about command must have an action.');
|
||||
}
|
||||
@@ -115,4 +123,31 @@ describe('aboutCommand', () => {
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not show ide client when it is not detected', async () => {
|
||||
vi.spyOn(mockContext.services.config!, 'getIdeClient').mockReturnValue({
|
||||
getDetectedIdeDisplayName: vi.fn().mockReturnValue(undefined),
|
||||
} as Partial<IdeClient> as IdeClient);
|
||||
|
||||
process.env['SANDBOX'] = '';
|
||||
if (!aboutCommand.action) {
|
||||
throw new Error('The about command must have an action.');
|
||||
}
|
||||
|
||||
await aboutCommand.action(mockContext, '');
|
||||
|
||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageType.ABOUT,
|
||||
cliVersion: 'test-version',
|
||||
osVersion: 'test-os',
|
||||
sandboxEnv: 'no sandbox',
|
||||
modelVersion: 'test-model',
|
||||
selectedAuthType: 'test-auth',
|
||||
gcpProject: 'test-gcp-project',
|
||||
ideClient: '',
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,18 +16,22 @@ export const aboutCommand: SlashCommand = {
|
||||
action: async (context) => {
|
||||
const osVersion = process.platform;
|
||||
let sandboxEnv = 'no sandbox';
|
||||
if (process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec') {
|
||||
sandboxEnv = process.env.SANDBOX;
|
||||
} else if (process.env.SANDBOX === 'sandbox-exec') {
|
||||
if (process.env['SANDBOX'] && process.env['SANDBOX'] !== 'sandbox-exec') {
|
||||
sandboxEnv = process.env['SANDBOX'];
|
||||
} else if (process.env['SANDBOX'] === 'sandbox-exec') {
|
||||
sandboxEnv = `sandbox-exec (${
|
||||
process.env.SEATBELT_PROFILE || 'unknown'
|
||||
process.env['SEATBELT_PROFILE'] || 'unknown'
|
||||
})`;
|
||||
}
|
||||
const modelVersion = context.services.config?.getModel() || 'Unknown';
|
||||
const cliVersion = await getCliVersion();
|
||||
const selectedAuthType =
|
||||
context.services.settings.merged.selectedAuthType || '';
|
||||
const gcpProject = process.env.GOOGLE_CLOUD_PROJECT || '';
|
||||
const gcpProject = process.env['GOOGLE_CLOUD_PROJECT'] || '';
|
||||
const ideClient =
|
||||
(context.services.config?.getIdeMode() &&
|
||||
context.services.config?.getIdeClient()?.getDetectedIdeDisplayName()) ||
|
||||
'';
|
||||
|
||||
const aboutItem: Omit<HistoryItemAbout, 'id'> = {
|
||||
type: MessageType.ABOUT,
|
||||
@@ -37,6 +41,7 @@ export const aboutCommand: SlashCommand = {
|
||||
modelVersion,
|
||||
selectedAuthType,
|
||||
gcpProject,
|
||||
ideClient,
|
||||
};
|
||||
|
||||
context.ui.addItem(aboutItem, Date.now());
|
||||
|
||||
@@ -16,6 +16,7 @@ import { formatMemoryUsage } from '../utils/formatters.js';
|
||||
vi.mock('open');
|
||||
vi.mock('../../utils/version.js');
|
||||
vi.mock('../utils/formatters.js');
|
||||
vi.mock('@qwen-code/qwen-code-core');
|
||||
vi.mock('node:process', () => ({
|
||||
default: {
|
||||
platform: 'test-platform',
|
||||
@@ -30,6 +31,9 @@ describe('bugCommand', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(getCliVersion).mockResolvedValue('0.1.0');
|
||||
vi.mocked(formatMemoryUsage).mockReturnValue('100 MB');
|
||||
vi.mock('@qwen-code/qwen-code-core', () => ({
|
||||
sessionId: 'test-session-id',
|
||||
}));
|
||||
vi.stubEnv('SANDBOX', 'qwen-test');
|
||||
});
|
||||
|
||||
@@ -44,6 +48,10 @@ describe('bugCommand', () => {
|
||||
config: {
|
||||
getModel: () => 'qwen3-coder-plus',
|
||||
getBugCommand: () => undefined,
|
||||
getIdeClient: () => ({
|
||||
getDetectedIdeDisplayName: () => 'VSCode',
|
||||
}),
|
||||
getIdeMode: () => true,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -54,10 +62,12 @@ describe('bugCommand', () => {
|
||||
const expectedInfo = `
|
||||
* **CLI Version:** 0.1.0
|
||||
* **Git Commit:** ${GIT_COMMIT_INFO}
|
||||
* **Session ID:** test-session-id
|
||||
* **Operating System:** test-platform v20.0.0
|
||||
* **Sandbox Environment:** test
|
||||
* **Model Version:** qwen3-coder-plus
|
||||
* **Memory Usage:** 100 MB
|
||||
* **IDE Client:** VSCode
|
||||
`;
|
||||
const expectedUrl =
|
||||
'https://github.com/QwenLM/qwen-code/issues/new?template=bug_report.yml&title=A%20test%20bug&info=' +
|
||||
@@ -74,6 +84,10 @@ describe('bugCommand', () => {
|
||||
config: {
|
||||
getModel: () => 'qwen3-coder-plus',
|
||||
getBugCommand: () => ({ urlTemplate: customTemplate }),
|
||||
getIdeClient: () => ({
|
||||
getDetectedIdeDisplayName: () => 'VSCode',
|
||||
}),
|
||||
getIdeMode: () => true,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -84,10 +98,12 @@ describe('bugCommand', () => {
|
||||
const expectedInfo = `
|
||||
* **CLI Version:** 0.1.0
|
||||
* **Git Commit:** ${GIT_COMMIT_INFO}
|
||||
* **Session ID:** test-session-id
|
||||
* **Operating System:** test-platform v20.0.0
|
||||
* **Sandbox Environment:** test
|
||||
* **Model Version:** qwen3-coder-plus
|
||||
* **Memory Usage:** 100 MB
|
||||
* **IDE Client:** VSCode
|
||||
`;
|
||||
const expectedUrl = customTemplate
|
||||
.replace('{title}', encodeURIComponent('A custom bug'))
|
||||
|
||||
@@ -15,6 +15,7 @@ import { MessageType } from '../types.js';
|
||||
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
|
||||
import { formatMemoryUsage } from '../utils/formatters.js';
|
||||
import { getCliVersion } from '../../utils/version.js';
|
||||
import { sessionId } from '@qwen-code/qwen-code-core';
|
||||
|
||||
export const bugCommand: SlashCommand = {
|
||||
name: 'bug',
|
||||
@@ -26,25 +27,33 @@ export const bugCommand: SlashCommand = {
|
||||
|
||||
const osVersion = `${process.platform} ${process.version}`;
|
||||
let sandboxEnv = 'no sandbox';
|
||||
if (process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec') {
|
||||
sandboxEnv = process.env.SANDBOX.replace(/^qwen-(?:code-)?/, '');
|
||||
} else if (process.env.SANDBOX === 'sandbox-exec') {
|
||||
if (process.env['SANDBOX'] && process.env['SANDBOX'] !== 'sandbox-exec') {
|
||||
sandboxEnv = process.env['SANDBOX'].replace(/^qwen-(?:code-)?/, '');
|
||||
} else if (process.env['SANDBOX'] === 'sandbox-exec') {
|
||||
sandboxEnv = `sandbox-exec (${
|
||||
process.env.SEATBELT_PROFILE || 'unknown'
|
||||
process.env['SEATBELT_PROFILE'] || 'unknown'
|
||||
})`;
|
||||
}
|
||||
const modelVersion = config?.getModel() || 'Unknown';
|
||||
const cliVersion = await getCliVersion();
|
||||
const memoryUsage = formatMemoryUsage(process.memoryUsage().rss);
|
||||
const ideClient =
|
||||
(context.services.config?.getIdeMode() &&
|
||||
context.services.config?.getIdeClient()?.getDetectedIdeDisplayName()) ||
|
||||
'';
|
||||
|
||||
const info = `
|
||||
let info = `
|
||||
* **CLI Version:** ${cliVersion}
|
||||
* **Git Commit:** ${GIT_COMMIT_INFO}
|
||||
* **Session ID:** ${sessionId}
|
||||
* **Operating System:** ${osVersion}
|
||||
* **Sandbox Environment:** ${sandboxEnv}
|
||||
* **Model Version:** ${modelVersion}
|
||||
* **Memory Usage:** ${memoryUsage}
|
||||
`;
|
||||
if (ideClient) {
|
||||
info += `* **IDE Client:** ${ideClient}\n`;
|
||||
}
|
||||
|
||||
let bugReportUrl =
|
||||
'https://github.com/QwenLM/qwen-code/issues/new?template=bug_report.yml&title={title}&info={info}';
|
||||
|
||||
@@ -185,30 +185,32 @@ describe('chatCommand', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should inform if conversation history is empty', async () => {
|
||||
it('should inform if conversation history is empty or only contains system context', async () => {
|
||||
mockGetHistory.mockReturnValue([]);
|
||||
const result = await saveCommand?.action?.(mockContext, tag);
|
||||
let result = await saveCommand?.action?.(mockContext, tag);
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'No conversation found to save.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should save the conversation if checkpoint does not exist', async () => {
|
||||
const history: HistoryItemWithoutId[] = [
|
||||
{
|
||||
type: 'user',
|
||||
text: 'hello',
|
||||
},
|
||||
];
|
||||
mockGetHistory.mockReturnValue(history);
|
||||
mockCheckpointExists.mockResolvedValue(false);
|
||||
mockGetHistory.mockReturnValue([
|
||||
{ role: 'user', parts: [{ text: 'context for our chat' }] },
|
||||
{ role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] },
|
||||
]);
|
||||
result = await saveCommand?.action?.(mockContext, tag);
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'No conversation found to save.',
|
||||
});
|
||||
|
||||
const result = await saveCommand?.action?.(mockContext, tag);
|
||||
|
||||
expect(mockCheckpointExists).toHaveBeenCalledWith(tag);
|
||||
expect(mockSaveCheckpoint).toHaveBeenCalledWith(history, tag);
|
||||
mockGetHistory.mockReturnValue([
|
||||
{ role: 'user', parts: [{ text: 'context for our chat' }] },
|
||||
{ role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] },
|
||||
{ role: 'user', parts: [{ text: 'Hello, how are you?' }] },
|
||||
]);
|
||||
result = await saveCommand?.action?.(mockContext, tag);
|
||||
expect(result).toEqual({
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
@@ -237,11 +239,11 @@ describe('chatCommand', () => {
|
||||
});
|
||||
|
||||
it('should save the conversation if overwrite is confirmed', async () => {
|
||||
const history: HistoryItemWithoutId[] = [
|
||||
{
|
||||
type: 'user',
|
||||
text: 'hello',
|
||||
},
|
||||
const history: Content[] = [
|
||||
{ role: 'user', parts: [{ text: 'context for our chat' }] },
|
||||
{ role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] },
|
||||
{ role: 'user', parts: [{ text: 'hello' }] },
|
||||
{ role: 'model', parts: [{ text: 'Hi there!' }] },
|
||||
];
|
||||
mockGetHistory.mockReturnValue(history);
|
||||
mockContext.overwriteConfirmed = true;
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
CommandKind,
|
||||
SlashCommandActionReturn,
|
||||
} from './types.js';
|
||||
import { decodeTagName } from '@qwen-code/qwen-code-core';
|
||||
import path from 'path';
|
||||
import { HistoryItemWithoutId, MessageType } from '../types.js';
|
||||
|
||||
@@ -41,8 +42,9 @@ const getSavedChatTags = async (
|
||||
if (file.startsWith(file_head) && file.endsWith(file_tail)) {
|
||||
const filePath = path.join(geminiDir, file);
|
||||
const stats = await fsPromises.stat(filePath);
|
||||
const tagName = file.slice(file_head.length, -file_tail.length);
|
||||
chatDetails.push({
|
||||
name: file.slice(file_head.length, -file_tail.length),
|
||||
name: decodeTagName(tagName),
|
||||
mtime: stats.mtime,
|
||||
});
|
||||
}
|
||||
@@ -142,12 +144,12 @@ const saveCommand: SlashCommand = {
|
||||
}
|
||||
|
||||
const history = chat.getHistory();
|
||||
if (history.length > 0) {
|
||||
if (history.length > 2) {
|
||||
await logger.saveCheckpoint(history, tag);
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: `Conversation checkpoint saved with tag: ${tag}.`,
|
||||
content: `Conversation checkpoint saved with tag: ${decodeTagName(tag)}.`,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
@@ -183,7 +185,7 @@ const resumeCommand: SlashCommand = {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: `No saved checkpoint found with tag: ${tag}.`,
|
||||
content: `No saved checkpoint found with tag: ${decodeTagName(tag)}.`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -252,13 +254,13 @@ const deleteCommand: SlashCommand = {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: `Conversation checkpoint '${tag}' has been deleted.`,
|
||||
content: `Conversation checkpoint '${decodeTagName(tag)}' has been deleted.`,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: `Error: No checkpoint found with tag '${tag}'.`,
|
||||
content: `Error: No checkpoint found with tag '${decodeTagName(tag)}'.`,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
@@ -55,7 +55,6 @@ describe('clearCommand', () => {
|
||||
expect(mockContext.ui.setDebugMessage).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(mockResetChat).toHaveBeenCalledTimes(1);
|
||||
expect(mockContext.session.resetSession).toHaveBeenCalledTimes(1);
|
||||
expect(uiTelemetryService.resetLastPromptTokenCount).toHaveBeenCalledTimes(
|
||||
1,
|
||||
);
|
||||
@@ -65,8 +64,6 @@ describe('clearCommand', () => {
|
||||
const setDebugMessageOrder = (mockContext.ui.setDebugMessage as Mock).mock
|
||||
.invocationCallOrder[0];
|
||||
const resetChatOrder = mockResetChat.mock.invocationCallOrder[0];
|
||||
const resetSessionOrder = (mockContext.session.resetSession as Mock).mock
|
||||
.invocationCallOrder[0];
|
||||
const resetTelemetryOrder = (
|
||||
uiTelemetryService.resetLastPromptTokenCount as Mock
|
||||
).mock.invocationCallOrder[0];
|
||||
@@ -76,8 +73,6 @@ describe('clearCommand', () => {
|
||||
expect(setDebugMessageOrder).toBeLessThan(resetChatOrder);
|
||||
expect(resetChatOrder).toBeLessThan(resetTelemetryOrder);
|
||||
expect(resetTelemetryOrder).toBeLessThan(clearOrder);
|
||||
expect(resetChatOrder).toBeLessThan(resetSessionOrder);
|
||||
expect(resetSessionOrder).toBeLessThan(clearOrder);
|
||||
});
|
||||
|
||||
it('should not attempt to reset chat if config service is not available', async () => {
|
||||
@@ -97,7 +92,6 @@ describe('clearCommand', () => {
|
||||
'Clearing terminal.',
|
||||
);
|
||||
expect(mockResetChat).not.toHaveBeenCalled();
|
||||
expect(nullConfigContext.session.resetSession).toHaveBeenCalledTimes(1);
|
||||
expect(uiTelemetryService.resetLastPromptTokenCount).toHaveBeenCalledTimes(
|
||||
1,
|
||||
);
|
||||
|
||||
@@ -24,7 +24,6 @@ export const clearCommand: SlashCommand = {
|
||||
}
|
||||
|
||||
uiTelemetryService.resetLastPromptTokenCount();
|
||||
context.session.resetSession();
|
||||
context.ui.clear();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -56,7 +56,7 @@ describe('docsCommand', () => {
|
||||
}
|
||||
|
||||
// Simulate a sandbox environment
|
||||
process.env.SANDBOX = 'gemini-sandbox';
|
||||
vi.stubEnv('SANDBOX', 'gemini-sandbox');
|
||||
const docsUrl = 'https://qwenlm.github.io/qwen-code-docs/en';
|
||||
|
||||
await docsCommand.action(mockContext, '');
|
||||
@@ -79,7 +79,7 @@ describe('docsCommand', () => {
|
||||
}
|
||||
|
||||
// Simulate the specific 'sandbox-exec' environment
|
||||
process.env.SANDBOX = 'sandbox-exec';
|
||||
vi.stubEnv('SANDBOX', 'sandbox-exec');
|
||||
const docsUrl = 'https://qwenlm.github.io/qwen-code-docs/en';
|
||||
|
||||
await docsCommand.action(mockContext, '');
|
||||
|
||||
@@ -20,7 +20,7 @@ export const docsCommand: SlashCommand = {
|
||||
action: async (context: CommandContext): Promise<void> => {
|
||||
const docsUrl = 'https://qwenlm.github.io/qwen-code-docs/en';
|
||||
|
||||
if (process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec') {
|
||||
if (process.env['SANDBOX'] && process.env['SANDBOX'] !== 'sandbox-exec') {
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
|
||||
@@ -69,16 +69,35 @@ describe('ideCommand', () => {
|
||||
vi.mocked(mockConfig.getIdeClient).mockReturnValue({
|
||||
getCurrentIde: () => DetectedIde.VSCode,
|
||||
getDetectedIdeDisplayName: () => 'VS Code',
|
||||
getConnectionStatus: () => ({
|
||||
status: core.IDEConnectionStatus.Disconnected,
|
||||
}),
|
||||
} as ReturnType<Config['getIdeClient']>);
|
||||
const command = ideCommand(mockConfig);
|
||||
expect(command).not.toBeNull();
|
||||
expect(command?.name).toBe('ide');
|
||||
expect(command?.subCommands).toHaveLength(3);
|
||||
expect(command?.subCommands?.[0].name).toBe('disable');
|
||||
expect(command?.subCommands?.[0].name).toBe('enable');
|
||||
expect(command?.subCommands?.[1].name).toBe('status');
|
||||
expect(command?.subCommands?.[2].name).toBe('install');
|
||||
});
|
||||
|
||||
it('should show disable command when connected', () => {
|
||||
vi.mocked(mockConfig.getIdeMode).mockReturnValue(true);
|
||||
vi.mocked(mockConfig.getIdeClient).mockReturnValue({
|
||||
getCurrentIde: () => DetectedIde.VSCode,
|
||||
getDetectedIdeDisplayName: () => 'VS Code',
|
||||
getConnectionStatus: () => ({
|
||||
status: core.IDEConnectionStatus.Connected,
|
||||
}),
|
||||
} as ReturnType<Config['getIdeClient']>);
|
||||
const command = ideCommand(mockConfig);
|
||||
expect(command).not.toBeNull();
|
||||
const subCommandNames = command?.subCommands?.map((cmd) => cmd.name);
|
||||
expect(subCommandNames).toContain('disable');
|
||||
expect(subCommandNames).not.toContain('enable');
|
||||
});
|
||||
|
||||
describe('status subcommand', () => {
|
||||
const mockGetConnectionStatus = vi.fn();
|
||||
beforeEach(() => {
|
||||
@@ -161,7 +180,9 @@ describe('ideCommand', () => {
|
||||
vi.mocked(mockConfig.getIdeMode).mockReturnValue(true);
|
||||
vi.mocked(mockConfig.getIdeClient).mockReturnValue({
|
||||
getCurrentIde: () => DetectedIde.VSCode,
|
||||
getConnectionStatus: vi.fn(),
|
||||
getConnectionStatus: () => ({
|
||||
status: core.IDEConnectionStatus.Disconnected,
|
||||
}),
|
||||
getDetectedIdeDisplayName: () => 'VS Code',
|
||||
} as unknown as ReturnType<Config['getIdeClient']>);
|
||||
vi.mocked(core.getIdeInstaller).mockReturnValue({
|
||||
@@ -199,7 +220,7 @@ describe('ideCommand', () => {
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
it('should show an error if installation fails', async () => {
|
||||
mockInstall.mockResolvedValue({
|
||||
|
||||
@@ -187,10 +187,6 @@ export const ideCommand = (config: Config | null): SlashCommand | null => {
|
||||
);
|
||||
|
||||
const result = await installer.install();
|
||||
if (result.success) {
|
||||
config.setIdeMode(true);
|
||||
context.services.settings.setValue(SettingScope.User, 'ideMode', true);
|
||||
}
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: result.success ? 'info' : 'error',
|
||||
@@ -198,6 +194,39 @@ export const ideCommand = (config: Config | null): SlashCommand | null => {
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
if (result.success) {
|
||||
context.services.settings.setValue(SettingScope.User, 'ideMode', true);
|
||||
// Poll for up to 5 seconds for the extension to activate.
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await config.setIdeModeAndSyncConnection(true);
|
||||
if (
|
||||
ideClient.getConnectionStatus().status ===
|
||||
IDEConnectionStatus.Connected
|
||||
) {
|
||||
break;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
const { messageType, content } = getIdeStatusMessage(ideClient);
|
||||
if (messageType === 'error') {
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: messageType,
|
||||
text: `Failed to automatically enable IDE integration. To fix this, run the CLI in a new terminal window.`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
} else {
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: messageType,
|
||||
text: content,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -237,13 +266,11 @@ export const ideCommand = (config: Config | null): SlashCommand | null => {
|
||||
},
|
||||
};
|
||||
|
||||
const ideModeEnabled = config.getIdeMode();
|
||||
if (ideModeEnabled) {
|
||||
ideSlashCommand.subCommands = [
|
||||
disableCommand,
|
||||
statusCommand,
|
||||
installCommand,
|
||||
];
|
||||
const { status } = ideClient.getConnectionStatus();
|
||||
const isConnected = status === IDEConnectionStatus.Connected;
|
||||
|
||||
if (isConnected) {
|
||||
ideSlashCommand.subCommands = [statusCommand, disableCommand];
|
||||
} else {
|
||||
ideSlashCommand.subCommands = [
|
||||
enableCommand,
|
||||
|
||||
@@ -73,7 +73,7 @@ describe('mcpCommand', () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Set up default mock environment
|
||||
delete process.env.SANDBOX;
|
||||
vi.unstubAllEnvs();
|
||||
|
||||
// Default mock implementations
|
||||
vi.mocked(getMCPServerStatus).mockReturnValue(MCPServerStatus.CONNECTED);
|
||||
@@ -83,7 +83,7 @@ describe('mcpCommand', () => {
|
||||
|
||||
// Create mock config with all necessary methods
|
||||
mockConfig = {
|
||||
getToolRegistry: vi.fn().mockResolvedValue({
|
||||
getToolRegistry: vi.fn().mockReturnValue({
|
||||
getAllTools: vi.fn().mockReturnValue([]),
|
||||
}),
|
||||
getMcpServers: vi.fn().mockReturnValue({}),
|
||||
@@ -119,7 +119,7 @@ describe('mcpCommand', () => {
|
||||
});
|
||||
|
||||
it('should show an error if tool registry is not available', async () => {
|
||||
mockConfig.getToolRegistry = vi.fn().mockResolvedValue(undefined);
|
||||
mockConfig.getToolRegistry = vi.fn().mockReturnValue(undefined);
|
||||
|
||||
const result = await mcpCommand.action!(mockContext, '');
|
||||
|
||||
@@ -133,7 +133,7 @@ describe('mcpCommand', () => {
|
||||
|
||||
describe('no MCP servers configured', () => {
|
||||
beforeEach(() => {
|
||||
mockConfig.getToolRegistry = vi.fn().mockResolvedValue({
|
||||
mockConfig.getToolRegistry = vi.fn().mockReturnValue({
|
||||
getAllTools: vi.fn().mockReturnValue([]),
|
||||
});
|
||||
mockConfig.getMcpServers = vi.fn().mockReturnValue({});
|
||||
@@ -184,7 +184,7 @@ describe('mcpCommand', () => {
|
||||
...mockServer3Tools,
|
||||
];
|
||||
|
||||
mockConfig.getToolRegistry = vi.fn().mockResolvedValue({
|
||||
mockConfig.getToolRegistry = vi.fn().mockReturnValue({
|
||||
getAllTools: vi.fn().mockReturnValue(allTools),
|
||||
});
|
||||
|
||||
@@ -243,7 +243,7 @@ describe('mcpCommand', () => {
|
||||
createMockMCPTool('tool2', 'server1', 'This is tool 2 description'),
|
||||
];
|
||||
|
||||
mockConfig.getToolRegistry = vi.fn().mockResolvedValue({
|
||||
mockConfig.getToolRegistry = vi.fn().mockReturnValue({
|
||||
getAllTools: vi.fn().mockReturnValue(mockServerTools),
|
||||
});
|
||||
|
||||
@@ -296,7 +296,7 @@ describe('mcpCommand', () => {
|
||||
createMockMCPTool('tool1', 'server1', 'This is tool 1 description'),
|
||||
];
|
||||
|
||||
mockConfig.getToolRegistry = vi.fn().mockResolvedValue({
|
||||
mockConfig.getToolRegistry = vi.fn().mockReturnValue({
|
||||
getAllTools: vi.fn().mockReturnValue(mockServerTools),
|
||||
});
|
||||
|
||||
@@ -340,7 +340,7 @@ describe('mcpCommand', () => {
|
||||
// Mock tools - only server1 has tools
|
||||
const mockServerTools = [createMockMCPTool('server1_tool1', 'server1')];
|
||||
|
||||
mockConfig.getToolRegistry = vi.fn().mockResolvedValue({
|
||||
mockConfig.getToolRegistry = vi.fn().mockReturnValue({
|
||||
getAllTools: vi.fn().mockReturnValue(mockServerTools),
|
||||
});
|
||||
|
||||
@@ -386,7 +386,7 @@ describe('mcpCommand', () => {
|
||||
createMockMCPTool('server2_tool1', 'server2'),
|
||||
];
|
||||
|
||||
mockConfig.getToolRegistry = vi.fn().mockResolvedValue({
|
||||
mockConfig.getToolRegistry = vi.fn().mockReturnValue({
|
||||
getAllTools: vi.fn().mockReturnValue(mockServerTools),
|
||||
});
|
||||
|
||||
@@ -523,7 +523,7 @@ describe('mcpCommand', () => {
|
||||
|
||||
const mockServerTools = [tool1, tool2];
|
||||
|
||||
mockConfig.getToolRegistry = vi.fn().mockResolvedValue({
|
||||
mockConfig.getToolRegistry = vi.fn().mockReturnValue({
|
||||
getAllTools: vi.fn().mockReturnValue(mockServerTools),
|
||||
});
|
||||
|
||||
@@ -566,7 +566,7 @@ describe('mcpCommand', () => {
|
||||
createMockMCPTool('tool1', 'server1', 'Tool without schema'),
|
||||
];
|
||||
|
||||
mockConfig.getToolRegistry = vi.fn().mockResolvedValue({
|
||||
mockConfig.getToolRegistry = vi.fn().mockReturnValue({
|
||||
getAllTools: vi.fn().mockReturnValue(mockServerTools),
|
||||
});
|
||||
|
||||
@@ -603,7 +603,7 @@ describe('mcpCommand', () => {
|
||||
createMockMCPTool('tool1', 'server1', 'Test tool'),
|
||||
];
|
||||
|
||||
mockConfig.getToolRegistry = vi.fn().mockResolvedValue({
|
||||
mockConfig.getToolRegistry = vi.fn().mockReturnValue({
|
||||
getAllTools: vi.fn().mockReturnValue(mockServerTools),
|
||||
});
|
||||
});
|
||||
@@ -766,7 +766,7 @@ describe('mcpCommand', () => {
|
||||
};
|
||||
|
||||
mockConfig.getMcpServers = vi.fn().mockReturnValue(mockMcpServers);
|
||||
mockConfig.getToolRegistry = vi.fn().mockResolvedValue({
|
||||
mockConfig.getToolRegistry = vi.fn().mockReturnValue({
|
||||
getAllTools: vi.fn().mockReturnValue([]),
|
||||
});
|
||||
|
||||
@@ -787,7 +787,7 @@ describe('mcpCommand', () => {
|
||||
};
|
||||
|
||||
mockConfig.getMcpServers = vi.fn().mockReturnValue(mockMcpServers);
|
||||
mockConfig.getToolRegistry = vi.fn().mockResolvedValue({
|
||||
mockConfig.getToolRegistry = vi.fn().mockReturnValue({
|
||||
getAllTools: vi.fn().mockReturnValue([]),
|
||||
});
|
||||
|
||||
@@ -879,7 +879,7 @@ describe('mcpCommand', () => {
|
||||
oauth: { enabled: true },
|
||||
},
|
||||
}),
|
||||
getToolRegistry: vi.fn().mockResolvedValue(mockToolRegistry),
|
||||
getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
|
||||
getGeminiClient: vi.fn().mockReturnValue(mockGeminiClient),
|
||||
getPromptRegistry: vi.fn().mockResolvedValue({
|
||||
removePromptsByServer: vi.fn(),
|
||||
@@ -972,6 +972,7 @@ describe('mcpCommand', () => {
|
||||
it('should refresh the list of tools and display the status', async () => {
|
||||
const mockToolRegistry = {
|
||||
discoverMcpTools: vi.fn(),
|
||||
restartMcpServers: vi.fn(),
|
||||
getAllTools: vi.fn().mockReturnValue([]),
|
||||
};
|
||||
const mockGeminiClient = {
|
||||
@@ -983,7 +984,7 @@ describe('mcpCommand', () => {
|
||||
config: {
|
||||
getMcpServers: vi.fn().mockReturnValue({ server1: {} }),
|
||||
getBlockedMcpServers: vi.fn().mockReturnValue([]),
|
||||
getToolRegistry: vi.fn().mockResolvedValue(mockToolRegistry),
|
||||
getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
|
||||
getGeminiClient: vi.fn().mockReturnValue(mockGeminiClient),
|
||||
getPromptRegistry: vi.fn().mockResolvedValue({
|
||||
getPromptsByServer: vi.fn().mockReturnValue([]),
|
||||
@@ -1004,11 +1005,11 @@ describe('mcpCommand', () => {
|
||||
expect(context.ui.addItem).toHaveBeenCalledWith(
|
||||
{
|
||||
type: 'info',
|
||||
text: 'Refreshing MCP servers and tools...',
|
||||
text: 'Restarting MCP servers...',
|
||||
},
|
||||
expect.any(Number),
|
||||
);
|
||||
expect(mockToolRegistry.discoverMcpTools).toHaveBeenCalled();
|
||||
expect(mockToolRegistry.restartMcpServers).toHaveBeenCalled();
|
||||
expect(mockGeminiClient.setTools).toHaveBeenCalled();
|
||||
expect(context.ui.reloadCommands).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -1039,7 +1040,7 @@ describe('mcpCommand', () => {
|
||||
});
|
||||
|
||||
it('should show an error if tool registry is not available', async () => {
|
||||
mockConfig.getToolRegistry = vi.fn().mockResolvedValue(undefined);
|
||||
mockConfig.getToolRegistry = vi.fn().mockReturnValue(undefined);
|
||||
|
||||
const refreshCommand = mcpCommand.subCommands?.find(
|
||||
(cmd) => cmd.name === 'refresh',
|
||||
|
||||
@@ -44,7 +44,7 @@ const getMcpStatus = async (
|
||||
};
|
||||
}
|
||||
|
||||
const toolRegistry = await config.getToolRegistry();
|
||||
const toolRegistry = config.getToolRegistry();
|
||||
if (!toolRegistry) {
|
||||
return {
|
||||
type: 'message',
|
||||
@@ -401,7 +401,7 @@ const authCommand: SlashCommand = {
|
||||
);
|
||||
|
||||
// Trigger tool re-discovery to pick up authenticated server
|
||||
const toolRegistry = await config.getToolRegistry();
|
||||
const toolRegistry = config.getToolRegistry();
|
||||
if (toolRegistry) {
|
||||
context.ui.addItem(
|
||||
{
|
||||
@@ -472,7 +472,7 @@ const listCommand: SlashCommand = {
|
||||
|
||||
const refreshCommand: SlashCommand = {
|
||||
name: 'refresh',
|
||||
description: 'Refresh the list of MCP servers and tools',
|
||||
description: 'Restarts MCP servers.',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
action: async (
|
||||
context: CommandContext,
|
||||
@@ -486,7 +486,7 @@ const refreshCommand: SlashCommand = {
|
||||
};
|
||||
}
|
||||
|
||||
const toolRegistry = await config.getToolRegistry();
|
||||
const toolRegistry = config.getToolRegistry();
|
||||
if (!toolRegistry) {
|
||||
return {
|
||||
type: 'message',
|
||||
@@ -498,12 +498,12 @@ const refreshCommand: SlashCommand = {
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: 'info',
|
||||
text: 'Refreshing MCP servers and tools...',
|
||||
text: 'Restarting MCP servers...',
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
|
||||
await toolRegistry.discoverMcpTools();
|
||||
await toolRegistry.restartMcpServers();
|
||||
|
||||
// Update the client with the new tools
|
||||
const geminiClient = config.getGeminiClient();
|
||||
|
||||
@@ -10,7 +10,11 @@ import fs from 'node:fs/promises';
|
||||
|
||||
import { vi, describe, expect, it, afterEach, beforeEach } from 'vitest';
|
||||
import * as gitUtils from '../../utils/gitUtils.js';
|
||||
import { setupGithubCommand } from './setupGithubCommand.js';
|
||||
import {
|
||||
setupGithubCommand,
|
||||
updateGitignore,
|
||||
GITHUB_WORKFLOW_PATHS,
|
||||
} from './setupGithubCommand.js';
|
||||
import { CommandContext, ToolActionReturn } from './types.js';
|
||||
import * as commandUtils from '../utils/commandUtils.js';
|
||||
|
||||
@@ -51,12 +55,7 @@ describe('setupGithubCommand', async () => {
|
||||
const fakeRepoRoot = scratchDir;
|
||||
const fakeReleaseVersion = 'v1.2.3';
|
||||
|
||||
const workflows = [
|
||||
'gemini-cli.yml',
|
||||
'gemini-issue-automated-triage.yml',
|
||||
'gemini-issue-scheduled-triage.yml',
|
||||
'gemini-pr-review.yml',
|
||||
];
|
||||
const workflows = GITHUB_WORKFLOW_PATHS.map((p) => path.basename(p));
|
||||
for (const workflow of workflows) {
|
||||
vi.mocked(global.fetch).mockReturnValueOnce(
|
||||
Promise.resolve(new Response(workflow)),
|
||||
@@ -102,5 +101,138 @@ describe('setupGithubCommand', async () => {
|
||||
const contents = await fs.readFile(workflowFile, 'utf8');
|
||||
expect(contents).toContain(workflow);
|
||||
}
|
||||
|
||||
// Verify that .gitignore was created with the expected entries
|
||||
const gitignorePath = path.join(scratchDir, '.gitignore');
|
||||
const gitignoreExists = await fs
|
||||
.access(gitignorePath)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
expect(gitignoreExists).toBe(true);
|
||||
|
||||
if (gitignoreExists) {
|
||||
const gitignoreContent = await fs.readFile(gitignorePath, 'utf8');
|
||||
expect(gitignoreContent).toContain('.gemini/');
|
||||
expect(gitignoreContent).toContain('gha-creds-*.json');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateGitignore', () => {
|
||||
let scratchDir = '';
|
||||
|
||||
beforeEach(async () => {
|
||||
scratchDir = await fs.mkdtemp(path.join(os.tmpdir(), 'update-gitignore-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (scratchDir) await fs.rm(scratchDir, { recursive: true });
|
||||
});
|
||||
|
||||
it('creates a new .gitignore file when none exists', async () => {
|
||||
await updateGitignore(scratchDir);
|
||||
|
||||
const gitignorePath = path.join(scratchDir, '.gitignore');
|
||||
const content = await fs.readFile(gitignorePath, 'utf8');
|
||||
|
||||
expect(content).toBe('.gemini/\ngha-creds-*.json\n');
|
||||
});
|
||||
|
||||
it('appends entries to existing .gitignore file', async () => {
|
||||
const gitignorePath = path.join(scratchDir, '.gitignore');
|
||||
const existingContent = '# Existing content\nnode_modules/\n';
|
||||
await fs.writeFile(gitignorePath, existingContent);
|
||||
|
||||
await updateGitignore(scratchDir);
|
||||
|
||||
const content = await fs.readFile(gitignorePath, 'utf8');
|
||||
|
||||
expect(content).toBe(
|
||||
'# Existing content\nnode_modules/\n\n.gemini/\ngha-creds-*.json\n',
|
||||
);
|
||||
});
|
||||
|
||||
it('does not add duplicate entries', async () => {
|
||||
const gitignorePath = path.join(scratchDir, '.gitignore');
|
||||
const existingContent = '.gemini/\nsome-other-file\ngha-creds-*.json\n';
|
||||
await fs.writeFile(gitignorePath, existingContent);
|
||||
|
||||
await updateGitignore(scratchDir);
|
||||
|
||||
const content = await fs.readFile(gitignorePath, 'utf8');
|
||||
|
||||
expect(content).toBe(existingContent);
|
||||
});
|
||||
|
||||
it('adds only missing entries when some already exist', async () => {
|
||||
const gitignorePath = path.join(scratchDir, '.gitignore');
|
||||
const existingContent = '.gemini/\nsome-other-file\n';
|
||||
await fs.writeFile(gitignorePath, existingContent);
|
||||
|
||||
await updateGitignore(scratchDir);
|
||||
|
||||
const content = await fs.readFile(gitignorePath, 'utf8');
|
||||
|
||||
// Should add only the missing gha-creds-*.json entry
|
||||
expect(content).toBe('.gemini/\nsome-other-file\n\ngha-creds-*.json\n');
|
||||
expect(content).toContain('gha-creds-*.json');
|
||||
// Should not duplicate .gemini/ entry
|
||||
expect((content.match(/\.gemini\//g) || []).length).toBe(1);
|
||||
});
|
||||
|
||||
it('does not get confused by entries in comments or as substrings', async () => {
|
||||
const gitignorePath = path.join(scratchDir, '.gitignore');
|
||||
const existingContent = [
|
||||
'# This is a comment mentioning .gemini/ folder',
|
||||
'my-app.gemini/config',
|
||||
'# Another comment with gha-creds-*.json pattern',
|
||||
'some-other-gha-creds-file.json',
|
||||
'',
|
||||
].join('\n');
|
||||
await fs.writeFile(gitignorePath, existingContent);
|
||||
|
||||
await updateGitignore(scratchDir);
|
||||
|
||||
const content = await fs.readFile(gitignorePath, 'utf8');
|
||||
|
||||
// Should add both entries since they don't actually exist as gitignore rules
|
||||
expect(content).toContain('.gemini/');
|
||||
expect(content).toContain('gha-creds-*.json');
|
||||
|
||||
// Verify the entries were added (not just mentioned in comments)
|
||||
const lines = content
|
||||
.split('\n')
|
||||
.map((line) => line.split('#')[0].trim())
|
||||
.filter((line) => line);
|
||||
expect(lines).toContain('.gemini/');
|
||||
expect(lines).toContain('gha-creds-*.json');
|
||||
expect(lines).toContain('my-app.gemini/config');
|
||||
expect(lines).toContain('some-other-gha-creds-file.json');
|
||||
});
|
||||
|
||||
it('handles file system errors gracefully', async () => {
|
||||
// Try to update gitignore in a non-existent directory
|
||||
const nonExistentDir = path.join(scratchDir, 'non-existent');
|
||||
|
||||
// This should not throw an error
|
||||
await expect(updateGitignore(nonExistentDir)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('handles permission errors gracefully', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
|
||||
|
||||
const fsModule = await import('node:fs');
|
||||
const writeFileSpy = vi
|
||||
.spyOn(fsModule.promises, 'writeFile')
|
||||
.mockRejectedValue(new Error('Permission denied'));
|
||||
|
||||
await expect(updateGitignore(scratchDir)).resolves.toBeUndefined();
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Failed to update .gitignore:',
|
||||
expect.any(Error),
|
||||
);
|
||||
|
||||
writeFileSpy.mockRestore();
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +24,14 @@ import {
|
||||
} from './types.js';
|
||||
import { getUrlOpenCommand } from '../../ui/utils/commandUtils.js';
|
||||
|
||||
export const GITHUB_WORKFLOW_PATHS = [
|
||||
'gemini-dispatch/gemini-dispatch.yml',
|
||||
'gemini-assistant/gemini-invoke.yml',
|
||||
'issue-triage/gemini-triage.yml',
|
||||
'issue-triage/gemini-scheduled-triage.yml',
|
||||
'pr-review/gemini-review.yml',
|
||||
];
|
||||
|
||||
// Generate OS-specific commands to open the GitHub pages needed for setup.
|
||||
function getOpenUrlsCommands(readmeUrl: string): string[] {
|
||||
// Determine the OS-specific command to open URLs, ex: 'open', 'xdg-open', etc
|
||||
@@ -44,6 +52,46 @@ function getOpenUrlsCommands(readmeUrl: string): string[] {
|
||||
return commands;
|
||||
}
|
||||
|
||||
// Add Gemini CLI specific entries to .gitignore file
|
||||
export async function updateGitignore(gitRepoRoot: string): Promise<void> {
|
||||
const gitignoreEntries = ['.gemini/', 'gha-creds-*.json'];
|
||||
|
||||
const gitignorePath = path.join(gitRepoRoot, '.gitignore');
|
||||
try {
|
||||
// Check if .gitignore exists and read its content
|
||||
let existingContent = '';
|
||||
let fileExists = true;
|
||||
try {
|
||||
existingContent = await fs.promises.readFile(gitignorePath, 'utf8');
|
||||
} catch (_error) {
|
||||
// File doesn't exist
|
||||
fileExists = false;
|
||||
}
|
||||
|
||||
if (!fileExists) {
|
||||
// Create new .gitignore file with the entries
|
||||
const contentToWrite = gitignoreEntries.join('\n') + '\n';
|
||||
await fs.promises.writeFile(gitignorePath, contentToWrite);
|
||||
} else {
|
||||
// Check which entries are missing
|
||||
const missingEntries = gitignoreEntries.filter(
|
||||
(entry) =>
|
||||
!existingContent
|
||||
.split(/\r?\n/)
|
||||
.some((line) => line.split('#')[0].trim() === entry),
|
||||
);
|
||||
|
||||
if (missingEntries.length > 0) {
|
||||
const contentToAdd = '\n' + missingEntries.join('\n') + '\n';
|
||||
await fs.promises.appendFile(gitignorePath, contentToAdd);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug('Failed to update .gitignore:', error);
|
||||
// Continue without failing the whole command
|
||||
}
|
||||
}
|
||||
|
||||
export const setupGithubCommand: SlashCommand = {
|
||||
name: 'setup-github',
|
||||
description: 'Set up GitHub Actions',
|
||||
@@ -91,15 +139,8 @@ export const setupGithubCommand: SlashCommand = {
|
||||
|
||||
// Download each workflow in parallel - there aren't enough files to warrant
|
||||
// a full workerpool model here.
|
||||
const workflows = [
|
||||
'gemini-cli/gemini-cli.yml',
|
||||
'issue-triage/gemini-issue-automated-triage.yml',
|
||||
'issue-triage/gemini-issue-scheduled-triage.yml',
|
||||
'pr-review/gemini-pr-review.yml',
|
||||
];
|
||||
|
||||
const downloads = [];
|
||||
for (const workflow of workflows) {
|
||||
for (const workflow of GITHUB_WORKFLOW_PATHS) {
|
||||
downloads.push(
|
||||
(async () => {
|
||||
const endpoint = `https://raw.githubusercontent.com/google-github-actions/run-gemini-cli/refs/tags/${releaseTag}/examples/workflows/${workflow}`;
|
||||
@@ -146,11 +187,14 @@ export const setupGithubCommand: SlashCommand = {
|
||||
abortController.abort();
|
||||
});
|
||||
|
||||
// Add entries to .gitignore file
|
||||
await updateGitignore(gitRepoRoot);
|
||||
|
||||
// Print out a message
|
||||
const commands = [];
|
||||
commands.push('set -eEuo pipefail');
|
||||
commands.push(
|
||||
`echo "Successfully downloaded ${workflows.length} workflows. Follow the steps in ${readmeUrl} (skipping the /setup-github step) to complete setup."`,
|
||||
`echo "Successfully downloaded ${GITHUB_WORKFLOW_PATHS.length} workflows and updated .gitignore. Follow the steps in ${readmeUrl} (skipping the /setup-github step) to complete setup."`,
|
||||
);
|
||||
commands.push(...getOpenUrlsCommands(readmeUrl));
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ describe('toolsCommand', () => {
|
||||
const mockContext = createMockCommandContext({
|
||||
services: {
|
||||
config: {
|
||||
getToolRegistry: () => Promise.resolve(undefined),
|
||||
getToolRegistry: () => undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -52,8 +52,7 @@ describe('toolsCommand', () => {
|
||||
const mockContext = createMockCommandContext({
|
||||
services: {
|
||||
config: {
|
||||
getToolRegistry: () =>
|
||||
Promise.resolve({ getAllTools: () => [] as Tool[] }),
|
||||
getToolRegistry: () => ({ getAllTools: () => [] as Tool[] }),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -73,8 +72,7 @@ describe('toolsCommand', () => {
|
||||
const mockContext = createMockCommandContext({
|
||||
services: {
|
||||
config: {
|
||||
getToolRegistry: () =>
|
||||
Promise.resolve({ getAllTools: () => mockTools }),
|
||||
getToolRegistry: () => ({ getAllTools: () => mockTools }),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -92,8 +90,7 @@ describe('toolsCommand', () => {
|
||||
const mockContext = createMockCommandContext({
|
||||
services: {
|
||||
config: {
|
||||
getToolRegistry: () =>
|
||||
Promise.resolve({ getAllTools: () => mockTools }),
|
||||
getToolRegistry: () => ({ getAllTools: () => mockTools }),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ export const toolsCommand: SlashCommand = {
|
||||
useShowDescriptions = true;
|
||||
}
|
||||
|
||||
const toolRegistry = await context.services.config?.getToolRegistry();
|
||||
const toolRegistry = context.services.config?.getToolRegistry();
|
||||
if (!toolRegistry) {
|
||||
context.ui.addItem(
|
||||
{
|
||||
|
||||
@@ -66,7 +66,6 @@ export interface CommandContext {
|
||||
// Session-specific data
|
||||
session: {
|
||||
stats: SessionStatsState;
|
||||
resetSession: () => void;
|
||||
/** A transient list of shell commands the user has approved for this session. */
|
||||
sessionShellAllowlist: Set<string>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user