Skip to content

Commit 8c95d64

Browse files
Merge branch 'develop'
2 parents 08da055 + c3ad1dc commit 8c95d64

File tree

5 files changed

+222
-12
lines changed

5 files changed

+222
-12
lines changed

README.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,19 @@ This will guide you through:
6161

6262
Check the installed version with:
6363

64+
```sh
65+
aicommits --version
6466
```
6567

66-
aicommits --version
68+
To update to the latest version, run:
6769

70+
```sh
71+
aicommits update
6872
```
6973

70-
If it's not the [latest version](https://github.com/Nutlope/aicommits/releases/latest), run:
74+
This will automatically detect your package manager (npm, pnpm, yarn, or bun) and update using the correct command.
75+
76+
Alternatively, you can manually update:
7177

7278
```sh
7379
npm install -g aicommits
@@ -233,6 +239,21 @@ This will:
233239
- Let you select from available models or enter a custom model name
234240
- Update your configuration automatically
235241

242+
### Updating aicommits
243+
244+
To update to the latest version, run:
245+
246+
```sh
247+
aicommits update
248+
```
249+
250+
This will:
251+
252+
- Check for the latest version on npm
253+
- Detect your package manager (npm, pnpm, yarn, or bun)
254+
- Update using the appropriate command
255+
- Show progress and confirm when complete
256+
236257
### Reading a configuration value
237258

238259
To retrieve a configuration option, use the command:

src/cli.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import setupCommand from './commands/setup.js';
1111
import modelCommand from './commands/model.js';
1212
import hookCommand, { isCalledFromGitHook } from './commands/hook.js';
1313
import prCommand from './commands/pr.js';
14+
import updateCommand from './commands/update.js';
1415
import { checkAndAutoUpdate } from './utils/auto-update.js';
1516
import { isHeadless } from './utils/headless.js';
1617

@@ -97,7 +98,7 @@ cli(
9798
},
9899
},
99100

100-
commands: [configCommand, setupCommand, modelCommand, hookCommand, prCommand],
101+
commands: [configCommand, setupCommand, modelCommand, hookCommand, prCommand, updateCommand],
101102

102103
help: {
103104
description,

src/commands/aicommits.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -247,16 +247,8 @@ export default async (
247247
} finally {
248248
if (s) {
249249
const duration = Date.now() - startTime;
250-
let tokensStr = '';
251-
if (usage?.total_tokens) {
252-
const tokens = usage.total_tokens;
253-
const formattedTokens =
254-
tokens >= 1000 ? `${(tokens / 1000).toFixed(0)}k` : tokens.toString();
255-
const speed = Math.round(tokens / (duration / 1000));
256-
tokensStr = `, ${formattedTokens} tokens (${speed} tokens/s)`;
257-
}
258250
s.stop(
259-
`✅ Changes analyzed in ${(duration / 1000).toFixed(1)}s${tokensStr}`
251+
`✅ Changes analyzed in ${(duration / 1000).toFixed(1)}s`
260252
);
261253
}
262254
}
@@ -319,6 +311,18 @@ export default async (
319311
}
320312
return;
321313
}
314+
315+
// Handle pre-commit hook failures or other git commit errors
316+
if (error.exitCode !== undefined) {
317+
outro(
318+
`${red('✘')} Commit failed. This may be due to pre-commit hooks.`
319+
);
320+
console.error(
321+
` ${dim('Use')} --no-verify ${dim('to bypass pre-commit hooks')}`
322+
);
323+
process.exit(1);
324+
}
325+
322326
throw error;
323327
}
324328
})().catch(handleCommandError);

src/commands/update.ts

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { command } from 'cleye';
2+
import { execSync, exec } from 'child_process';
3+
import { promisify } from 'util';
4+
import { green, red, yellow, cyan } from 'kolorist';
5+
import { outro, spinner } from '@clack/prompts';
6+
import pkg from '../../package.json';
7+
import { handleCommandError, KnownError } from '../utils/error.js';
8+
9+
const execAsync = promisify(exec);
10+
11+
interface PackageManagerInfo {
12+
name: string;
13+
updateCommand: string;
14+
}
15+
16+
// Determine the dist tag based on current version
17+
// Versions with prerelease (e.g., 2.0.0-develop.5) use 'develop' tag
18+
// Stable versions use 'latest' tag
19+
function getDistTag(version: string): string {
20+
// Skip for development/semantic-release versions
21+
if (version === '0.0.0-semantic-release' || version.includes('semantic-release')) {
22+
return 'latest';
23+
}
24+
// If version has prerelease identifier (contains '-'), use 'develop' tag
25+
if (version.includes('-')) {
26+
return 'develop';
27+
}
28+
return 'latest';
29+
}
30+
31+
function detectPackageManager(distTag: string): PackageManagerInfo {
32+
// Check if running from global installation
33+
try {
34+
const globalPath = execSync('npm root -g', { encoding: 'utf8' }).trim();
35+
const { execPath } = process;
36+
37+
// Check if running from global npm installation
38+
if (execPath.includes(globalPath) || execPath.includes('/usr/local') || execPath.includes('/usr/bin')) {
39+
return { name: 'npm', updateCommand: `npm install -g aicommits@${distTag}` };
40+
}
41+
} catch {
42+
// Fall through to other detection methods
43+
}
44+
45+
// Check for pnpm
46+
try {
47+
execSync('pnpm --version', { stdio: 'ignore' });
48+
// Check if installed via pnpm global
49+
const pnpmList = execSync('pnpm list -g aicommits', { encoding: 'utf8' });
50+
if (pnpmList.includes('aicommits')) {
51+
return { name: 'pnpm', updateCommand: `pnpm add -g aicommits@${distTag}` };
52+
}
53+
} catch {
54+
// Not pnpm
55+
}
56+
57+
// Check for yarn
58+
try {
59+
execSync('yarn --version', { stdio: 'ignore' });
60+
// Check if installed via yarn global
61+
const yarnList = execSync('yarn global list', { encoding: 'utf8' });
62+
if (yarnList.includes('aicommits')) {
63+
return { name: 'yarn', updateCommand: `yarn global add aicommits@${distTag}` };
64+
}
65+
} catch {
66+
// Not yarn
67+
}
68+
69+
// Check for bun
70+
try {
71+
execSync('bun --version', { stdio: 'ignore' });
72+
// Check if installed via bun
73+
const bunList = execSync('bun pm bin -g', { encoding: 'utf8' });
74+
if (process.execPath.includes('bun') || bunList.includes('aicommits')) {
75+
return { name: 'bun', updateCommand: `bun add -g aicommits@${distTag}` };
76+
}
77+
} catch {
78+
// Not bun
79+
}
80+
81+
// Default to npm
82+
return { name: 'npm', updateCommand: `npm install -g aicommits@${distTag}` };
83+
}
84+
85+
async function getLatestVersion(distTag: string): Promise<string | null> {
86+
try {
87+
const response = await fetch(`https://registry.npmjs.org/aicommits/${distTag}`, {
88+
headers: { Accept: 'application/json' },
89+
});
90+
if (!response.ok) return null;
91+
const data = await response.json();
92+
return data.version || null;
93+
} catch {
94+
return null;
95+
}
96+
}
97+
98+
export default command(
99+
{
100+
name: 'update',
101+
description: 'Update aicommits to the latest version',
102+
help: {
103+
description: 'Check for updates and install the latest version using your package manager',
104+
},
105+
},
106+
() => {
107+
(async () => {
108+
// Determine dist tag based on current version
109+
const distTag = getDistTag(pkg.version);
110+
const pm = detectPackageManager(distTag);
111+
112+
console.log(`${cyan('ℹ')} Current version: ${pkg.version}`);
113+
console.log(`${cyan('ℹ')} Package manager detected: ${pm.name}`);
114+
if (distTag !== 'latest') {
115+
console.log(`${cyan('ℹ')} Using '${distTag}' distribution tag`);
116+
}
117+
118+
const s = spinner();
119+
s.start('Checking for updates...');
120+
121+
const latestVersion = await getLatestVersion(distTag);
122+
123+
if (!latestVersion) {
124+
s.stop('Could not check for updates', 1);
125+
throw new KnownError('Failed to fetch latest version from npm registry');
126+
}
127+
128+
if (latestVersion === pkg.version) {
129+
s.stop(`${green('✔')} Already on the latest version (${pkg.version})`);
130+
return;
131+
}
132+
133+
s.stop(`${green('✔')} Update available: v${pkg.version} → v${latestVersion}`);
134+
135+
const updateS = spinner();
136+
updateS.start(`Updating via ${pm.name}...`);
137+
138+
try {
139+
await execAsync(pm.updateCommand, { timeout: 120000 });
140+
141+
updateS.stop(`${green('✔')} Successfully updated to v${latestVersion}`);
142+
outro(`${green('✔')} Update complete! Run 'aic --version' to verify.`);
143+
} catch (error: any) {
144+
updateS.stop(`${red('✘')} Update failed`, 1);
145+
146+
if (error.stderr?.includes('permission') || error.message?.includes('permission')) {
147+
console.error(`${red('✘')} Permission denied. Try running with sudo:`);
148+
console.error(` sudo ${pm.updateCommand}`);
149+
} else if (error.stderr?.includes('EACCES')) {
150+
console.error(`${red('✘')} Permission denied. Try running with sudo:`);
151+
console.error(` sudo ${pm.updateCommand}`);
152+
} else {
153+
console.error(`${red('✘')} Error: ${error.message || 'Unknown error'}`);
154+
console.error(`\n${yellow('You can manually update with:')}`);
155+
console.error(` ${pm.updateCommand}`);
156+
}
157+
158+
process.exit(1);
159+
}
160+
})().catch(handleCommandError);
161+
}
162+
);

src/utils/openai.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,17 @@ export const generateCommitMessage = async ({
182182
} catch (error) {
183183
const errorAsAny = error as any;
184184

185+
// Handle AbortController timeout
186+
if (
187+
errorAsAny.name === 'AbortError' ||
188+
errorAsAny.message?.includes('aborted') ||
189+
errorAsAny.message?.includes('This operation was aborted')
190+
) {
191+
throw new KnownError(
192+
`Request timed out after ${timeout / 1000} seconds. The API took too long to respond. Try again or use a different model.`
193+
);
194+
}
195+
185196
if (errorAsAny.code === 'ENOTFOUND') {
186197
throw new KnownError(
187198
`Error connecting to ${errorAsAny.hostname} (${errorAsAny.syscall}). Are you connected to the internet?`
@@ -290,6 +301,17 @@ Do not add thanks, explanations, or any text outside the commit message.`;
290301
} catch (error) {
291302
const errorAsAny = error as any;
292303

304+
// Handle AbortController timeout
305+
if (
306+
errorAsAny.name === 'AbortError' ||
307+
errorAsAny.message?.includes('aborted') ||
308+
errorAsAny.message?.includes('This operation was aborted')
309+
) {
310+
throw new KnownError(
311+
`Request timed out after ${timeout / 1000} seconds. The API took too long to respond. Try again or use a different model.`
312+
);
313+
}
314+
293315
throw errorAsAny;
294316
}
295317
};

0 commit comments

Comments
 (0)