Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 0 additions & 119 deletions .github/workflows/release.yml

This file was deleted.

2 changes: 1 addition & 1 deletion .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ tsup.config.prod.ts
.prettierrc

# Testing
tests/
test/
coverage/
*.test.ts
*.spec.ts
Expand Down
23 changes: 22 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Removed
- Code coverage dependencies and scripts (`vitest coverage`, `c8`, `jest`)
- Duplicate npm scripts (consolidated lint/format variants)
- Duplicate npm scripts (consolidated lint/format variants)

## [1.1.0] - 2025-07-06

### Added
- **Email Masking**: All email addresses are now masked by default in command outputs (e.g., `u***r@e****e.com`)
- **SSH Key Fingerprints**: `auth` command displays SSH key fingerprint (SHA256) instead of the full public key
- **Windows File Permissions**: Implemented ACL-based permissions for SSH config and key files on Windows using `icacls`

### Changed
- **Profile Auto-detection**: Now requires user confirmation before applying detected profiles
- **Account Selection**: Removed email addresses from selection dropdowns in `clone` and `init` commands
- **SSH Key Info**: Removed comment field from SSH key information display

### Fixed
- SSH key generation failing due to missing comment parameter in ssh-keygen command
- Windows SSH config files not receiving restrictive permissions

### Security
- Enhanced privacy protection by masking email addresses throughout the application
- Reduced risk of accidental SSH key exposure by showing only fingerprints
- Improved Windows security with proper file permissions for SSH-related files
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,17 +407,28 @@ ssh -T git@github.com-personal

### Windows: SSH Agent Not Running

If you see "Could not add key to ssh-agent" on Windows:
If you see "Could not add key to ssh-agent" on Windows, the SSH agent service is likely not running.

```bash
# Start ssh-agent service (run as Administrator)
**To fix this permanently:**

```powershell
# 1. Open PowerShell as Administrator
# 2. Enable and start the SSH agent service
Get-Service ssh-agent | Set-Service -StartupType Automatic
Start-Service ssh-agent

# Or manually add your key
# 3. Verify it's running
Get-Service ssh-agent
```

**Alternative: Manually add your key**
```bash
# If the above doesn't work, manually add your key
ssh-add C:\Users\YourName\.ssh\gitm_personal_github
```

**Note:** This is a one-time setup. Once enabled, the SSH agent will start automatically with Windows.

### Port 22 Blocked

If your network blocks port 22:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@loopgrid/gitm",
"version": "1.0.2",
"version": "1.1.0",
"description": "Seamlessly manage multiple git accounts on the same device",
"main": "dist/cli.js",
"bin": {
Expand Down
4 changes: 3 additions & 1 deletion src/commands/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,9 @@ export async function addAccount(profile: string, options?: { provider?: string

if (answers.generateSSH) {
try {
await generateSSHKey(sshKeyPath, answers.email, profile);
await generateSSHKey(sshKeyPath, answers.email, {
comment: `${answers.email} (gitm: ${profile})`,
});
logSuccess('SSH key generated successfully');
} catch (error) {
logError(`Failed to generate SSH key: ${(error as Error).message}`);
Expand Down
8 changes: 5 additions & 3 deletions src/commands/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getAccount } from '@/lib/config';
import { readPublicKey, updateSSHConfig } from '@/utils/ssh';
import { readPublicKey, updateSSHConfig, getSSHKeyInfo } from '@/utils/ssh';
import { logError, logSuccess, logInfo, log, LogLevel } from '@/utils/cli';
import { getAuthState, updateAuthState, clearAuthState, isAuthCompleted } from '@/utils/auth-state';
import { safeExec } from '@/utils/shell';
Expand Down Expand Up @@ -46,9 +46,11 @@

try {
const publicKey = await readPublicKey(account.sshKeyPath);
const keyInfo = getSSHKeyInfo(publicKey);

console.log(chalk.bold('\nSSH Public Key for ' + profile + ':\n'));
console.log(chalk.gray(publicKey));
console.log(chalk.bold('\nSSH Key Details for ' + profile + ':\n'));
console.log(chalk.cyan('Type: ') + keyInfo.type);
console.log(chalk.cyan('Fingerprint: ') + keyInfo.fingerprint);

const providerUrls: Record<string, string> = {
github: 'https://github.com/settings/keys',
Expand Down Expand Up @@ -335,7 +337,7 @@
log(chalk.gray(output.trim()), LogLevel.PLAIN);
return false;
}
} catch (error: any) {

Check warning on line 340 in src/commands/auth.ts

View workflow job for this annotation

GitHub Actions / Validate Code Quality, Tests & Security

Unexpected any. Specify a different type
// SSH test commands often "fail" with exit code 1 even on success
const output = error.stdout || error.stderr || error.message;

Expand Down
41 changes: 35 additions & 6 deletions src/commands/clone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { detectAccountForRepo } from '@/utils/git';
import { getSSHRemoteUrl, updateSSHConfig, addSSHKeyToAgent } from '@/utils/ssh';
import { getAccount, getAccounts } from '@/lib/config';
import { CloneOptions } from '@/types';
import { logError, logSuccess, logWarning, logInfo } from '@/utils/cli';
import { logError, logSuccess, logWarning, logInfo, maskEmail } from '@/utils/cli';

/**
* Clone a repository with the appropriate account
Expand Down Expand Up @@ -35,17 +35,46 @@ export async function cloneRepo(
let selectedAccount;

if (detection.profile) {
selectedProfile = detection.profile;
selectedAccount = detection.account;
logSuccess(`Auto-detected account: ${selectedProfile}`);
logSuccess(`Auto-detected account: ${detection.profile}`);

const { confirmDetection } = await inquirer.prompt<{ confirmDetection: boolean }>([
{
type: 'confirm',
name: 'confirmDetection',
message: `Use auto-detected account '${detection.profile}'?`,
default: true,
},
]);

if (confirmDetection) {
selectedProfile = detection.profile;
selectedAccount = detection.account;
} else {
// Show all available accounts for selection
const allAccounts = Object.entries(accounts);
const { profile } = await inquirer.prompt<{ profile: string }>([
{
type: 'list',
name: 'profile',
message: 'Select account to use for this repository:',
choices: allAccounts.map(([profileName, account]) => ({
name: `${profileName} (${account.name})`,
value: profileName,
})),
},
]);

selectedProfile = profile;
selectedAccount = getAccount(profile);
}
} else if (detection.candidates && detection.candidates.length > 0) {
const { profile } = await inquirer.prompt<{ profile: string }>([
{
type: 'list',
name: 'profile',
message: 'Select account to use for this repository:',
choices: detection.candidates.map(([profileName, account]) => ({
name: `${profileName} (${account.name} - ${account.email})`,
name: `${profileName} (${account.name})`,
value: profileName,
})),
},
Expand Down Expand Up @@ -103,7 +132,7 @@ export async function cloneRepo(

logSuccess(`Git config set for account '${selectedProfile}'`);
logInfo(`Repository: ${path.resolve(targetDir)}`);
logInfo(`Account: ${selectedAccount.name} <${selectedAccount.email}>`);
logInfo(`Account: ${selectedAccount.name} <${maskEmail(selectedAccount.email)}>`);

if (!useSSH && url.startsWith('https://')) {
logInfo("Note: Cloned with HTTPS. Use 'gitm use <profile>' to switch to SSH");
Expand Down
35 changes: 32 additions & 3 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import inquirer from 'inquirer';
import { getCurrentRepo, detectAccountForRepo, applyAccountToRepo } from '@/utils/git';
import { logError, logSuccess, logInfo, logWarning } from '@/utils/cli';
import { getAccounts } from '@/lib/config';

/**
* Initialize repository with auto-detected or selected account
Expand All @@ -14,7 +15,6 @@ export async function initRepo(options: { ssh?: boolean } = { ssh: true }): Prom

if (!detection) {
// Let's check if accounts actually exist
const { getAccounts } = await import('@/lib/config');
const accounts = getAccounts();
const accountCount = Object.keys(accounts).length;

Expand All @@ -32,15 +32,44 @@ export async function initRepo(options: { ssh?: boolean } = { ssh: true }): Prom

if (detection.profile) {
logSuccess(`Auto-detected account: ${detection.profile}`);
selectedProfile = detection.profile;

const { confirmDetection } = await inquirer.prompt<{ confirmDetection: boolean }>([
{
type: 'confirm',
name: 'confirmDetection',
message: `Use auto-detected account '${detection.profile}'?`,
default: true,
},
]);

if (confirmDetection) {
selectedProfile = detection.profile;
} else {
// Show all available accounts for selection
const accounts = getAccounts();
const allAccounts = Object.entries(accounts);
const { profile } = await inquirer.prompt<{ profile: string }>([
{
type: 'list',
name: 'profile',
message: 'Select account to use for this repository:',
choices: allAccounts.map(([profileName, account]) => ({
name: `${profileName} (${account.name})`,
value: profileName,
})),
},
]);

selectedProfile = profile;
}
} else if (detection.candidates && detection.candidates.length > 0) {
const { profile } = await inquirer.prompt<{ profile: string }>([
{
type: 'list',
name: 'profile',
message: 'Select account to use for this repository:',
choices: detection.candidates.map(([profileName, account]) => ({
name: `${profileName} (${account.name} - ${account.email})`,
name: `${profileName} (${account.name})`,
value: profileName,
})),
},
Expand Down
4 changes: 2 additions & 2 deletions src/commands/list.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getAccounts } from '@/lib/config';
import { log, logWarning, logInfo, sectionHeader, formatKeyValue } from '@/utils/cli';
import { log, logWarning, logInfo, sectionHeader, formatKeyValue, maskEmail } from '@/utils/cli';
import chalk from 'chalk';

/**
Expand All @@ -21,7 +21,7 @@ export function listAccounts(): void {
const account = accounts[profile];
log(chalk.cyan(` ${profile}`));
log(formatKeyValue('Name', account.name, 4));
log(formatKeyValue('Email', account.email, 4));
log(formatKeyValue('Email', maskEmail(account.email), 4));
log(formatKeyValue('Provider', account.provider, 4));
log(formatKeyValue('Username', account.username, 4));
console.log();
Expand Down
3 changes: 2 additions & 1 deletion src/commands/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
sectionHeader,
formatKeyValue,
logInfo,
maskEmail,
} from '@/utils/cli';
import chalk from 'chalk';

Expand Down Expand Up @@ -38,7 +39,7 @@ export async function showStatus(): Promise<void> {
if (gitUser.name && gitUser.email) {
console.log(chalk.green('Git User Configuration:'));
console.log(formatKeyValue('Name', gitUser.name));
console.log(formatKeyValue('Email', gitUser.email));
console.log(formatKeyValue('Email', maskEmail(gitUser.email)));
} else {
logWarning('No git user configured for this repository');
}
Expand Down
Loading
Loading