summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--packages/cli/src/ui/commands/setupGithubCommand.test.ts135
-rw-r--r--packages/cli/src/ui/commands/setupGithubCommand.ts45
3 files changed, 181 insertions, 2 deletions
diff --git a/.gitignore b/.gitignore
index fbbb2dcc..bcead317 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,3 +40,6 @@ packages/cli/src/generated/
packages/core/src/generated/
.integration-tests/
packages/vscode-ide-companion/*.vsix
+
+# GHA credentials
+gha-creds-*.json
diff --git a/packages/cli/src/ui/commands/setupGithubCommand.test.ts b/packages/cli/src/ui/commands/setupGithubCommand.test.ts
index 38589e58..2dd9f97f 100644
--- a/packages/cli/src/ui/commands/setupGithubCommand.test.ts
+++ b/packages/cli/src/ui/commands/setupGithubCommand.test.ts
@@ -10,7 +10,7 @@ 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 } from './setupGithubCommand.js';
import { CommandContext, ToolActionReturn } from './types.js';
import * as commandUtils from '../utils/commandUtils.js';
@@ -102,5 +102,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();
});
});
diff --git a/packages/cli/src/ui/commands/setupGithubCommand.ts b/packages/cli/src/ui/commands/setupGithubCommand.ts
index 2f024e60..8ddb5cd9 100644
--- a/packages/cli/src/ui/commands/setupGithubCommand.ts
+++ b/packages/cli/src/ui/commands/setupGithubCommand.ts
@@ -44,6 +44,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',
@@ -146,11 +186,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 ${workflows.length} workflows and updated .gitignore. Follow the steps in ${readmeUrl} (skipping the /setup-github step) to complete setup."`,
);
commands.push(...getOpenUrlsCommands(readmeUrl));