@hifullmoon/aicommit 2.2.3 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.aicommit.config.example.json +31 -8
- package/CHANGELOG.md +25 -1
- package/README.md +41 -6
- package/README.zh-CN.md +41 -6
- package/bin/aicommit.js +6 -1
- package/docs/distribution.md +8 -1
- package/docs/large-change-implementation-plan.md +193 -0
- package/docs/privacy.md +6 -0
- package/docs/provider-compatibility.md +44 -16
- package/docs/troubleshooting.md +12 -0
- package/package.json +4 -2
- package/schemas/aicommit-output.schema.json +120 -18
- package/src/analysis-budget.js +76 -0
- package/src/api.js +6 -316
- package/src/change-analysis.js +558 -0
- package/src/cli.js +31 -5
- package/src/completion.js +3 -0
- package/src/config.js +16 -0
- package/src/doctor.js +2 -4
- package/src/git-spool.js +108 -0
- package/src/git.js +25 -4
- package/src/local-analysis.js +271 -0
- package/src/main.js +120 -27
- package/src/model-client.js +403 -0
- package/src/provider-response.js +148 -0
- package/src/providers.js +134 -206
- package/src/runtime.js +6 -0
- package/src/split.js +194 -43
- package/src/update.js +326 -0
package/src/update.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
|
|
9
|
+
import { ERROR_CATEGORIES, fail } from './errors.js';
|
|
10
|
+
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
const manifest = require('../package.json');
|
|
13
|
+
|
|
14
|
+
export const PACKAGE_NAME = manifest.name;
|
|
15
|
+
export const CURRENT_VERSION = manifest.version;
|
|
16
|
+
|
|
17
|
+
const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
18
|
+
const SEMVER =
|
|
19
|
+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
|
|
20
|
+
const NETWORK_CODES = new Set([
|
|
21
|
+
'ECONNRESET',
|
|
22
|
+
'ECONNREFUSED',
|
|
23
|
+
'EHOSTUNREACH',
|
|
24
|
+
'ENETUNREACH',
|
|
25
|
+
'ENOTFOUND',
|
|
26
|
+
'ETIMEDOUT',
|
|
27
|
+
'EAI_AGAIN',
|
|
28
|
+
]);
|
|
29
|
+
const PERMISSION_CODES = new Set(['EACCES', 'EPERM']);
|
|
30
|
+
|
|
31
|
+
function parseVersion(version) {
|
|
32
|
+
const match = SEMVER.exec(version || '');
|
|
33
|
+
if (!match) throw new Error(`Invalid semantic version: ${version || '(empty)'}`);
|
|
34
|
+
const prerelease = match[4]?.split('.') || [];
|
|
35
|
+
if (prerelease.some((part) => /^\d+$/.test(part) && part.length > 1 && part.startsWith('0'))) {
|
|
36
|
+
throw new Error(`Invalid semantic version: ${version}`);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
major: Number(match[1]),
|
|
40
|
+
minor: Number(match[2]),
|
|
41
|
+
patch: Number(match[3]),
|
|
42
|
+
prerelease,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function compareIdentifiers(left, right) {
|
|
47
|
+
const leftNumeric = /^\d+$/.test(left);
|
|
48
|
+
const rightNumeric = /^\d+$/.test(right);
|
|
49
|
+
if (leftNumeric && rightNumeric) return Number(left) - Number(right);
|
|
50
|
+
if (leftNumeric) return -1;
|
|
51
|
+
if (rightNumeric) return 1;
|
|
52
|
+
if (left === right) return 0;
|
|
53
|
+
return left < right ? -1 : 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function compareVersions(leftVersion, rightVersion) {
|
|
57
|
+
const left = parseVersion(leftVersion);
|
|
58
|
+
const right = parseVersion(rightVersion);
|
|
59
|
+
for (const field of ['major', 'minor', 'patch']) {
|
|
60
|
+
if (left[field] !== right[field]) return left[field] - right[field];
|
|
61
|
+
}
|
|
62
|
+
if (left.prerelease.length === 0 && right.prerelease.length === 0) return 0;
|
|
63
|
+
if (left.prerelease.length === 0) return 1;
|
|
64
|
+
if (right.prerelease.length === 0) return -1;
|
|
65
|
+
const length = Math.max(left.prerelease.length, right.prerelease.length);
|
|
66
|
+
for (let index = 0; index < length; index += 1) {
|
|
67
|
+
if (left.prerelease[index] === undefined) return -1;
|
|
68
|
+
if (right.prerelease[index] === undefined) return 1;
|
|
69
|
+
const comparison = compareIdentifiers(left.prerelease[index], right.prerelease[index]);
|
|
70
|
+
if (comparison !== 0) return comparison;
|
|
71
|
+
}
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function npmInvocation(args, platform = process.platform, env = process.env) {
|
|
76
|
+
if (platform !== 'win32') return { command: 'npm', args };
|
|
77
|
+
if (!args.every((arg) => /^[0-9A-Za-z@_./:-]+$/.test(arg))) {
|
|
78
|
+
throw new Error('Refusing to pass an unsafe argument to npm.cmd.');
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
command: env.ComSpec || env.COMSPEC || 'cmd.exe',
|
|
82
|
+
args: ['/d', '/c', `npm.cmd ${args.join(' ')}`],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function runNpm(args, options = {}) {
|
|
87
|
+
const platform = options.platform || process.platform;
|
|
88
|
+
const invocation = npmInvocation(args, platform, options.env || process.env);
|
|
89
|
+
return spawnSync(invocation.command, invocation.args, {
|
|
90
|
+
encoding: 'utf8',
|
|
91
|
+
env: options.env || process.env,
|
|
92
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
93
|
+
timeout: args[0] === 'install' ? 300_000 : 30_000,
|
|
94
|
+
windowsHide: true,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function resultText(result) {
|
|
99
|
+
return `${result?.stderr || ''}\n${result?.stdout || ''}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function npmErrorCode(result) {
|
|
103
|
+
const text = resultText(result);
|
|
104
|
+
const npmCode = /(?:npm ERR!|npm error)\s+code\s+([A-Z0-9_-]+)/i.exec(text)?.[1];
|
|
105
|
+
if (npmCode) return npmCode.toUpperCase();
|
|
106
|
+
return /\b(EACCES|EPERM|ECONNRESET|ECONNREFUSED|EHOSTUNREACH|ENETUNREACH|ENOTFOUND|ETIMEDOUT|EAI_AGAIN)\b/i
|
|
107
|
+
.exec(text)?.[1]
|
|
108
|
+
?.toUpperCase();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function runCheckedNpm(args, runner, action, debug, log) {
|
|
112
|
+
if (debug) log(chalk.dim(` npm ${args.join(' ')}`));
|
|
113
|
+
let result;
|
|
114
|
+
try {
|
|
115
|
+
result = runner(args);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
throw fail(ERROR_CATEGORIES.CONFIG, `Unable to start npm while ${action}.`, { cause: error });
|
|
118
|
+
}
|
|
119
|
+
if (result?.error?.code === 'ENOENT') {
|
|
120
|
+
throw fail(
|
|
121
|
+
ERROR_CATEGORIES.CONFIG,
|
|
122
|
+
'npm is unavailable in PATH. Install npm or select the Node.js environment that installed aicommit.',
|
|
123
|
+
{ cause: result.error },
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (result?.error) {
|
|
127
|
+
const category = NETWORK_CODES.has(result.error.code)
|
|
128
|
+
? ERROR_CATEGORIES.NETWORK
|
|
129
|
+
: ERROR_CATEGORIES.CONFIG;
|
|
130
|
+
throw fail(category, `npm failed while ${action} (${result.error.code || 'unknown error'}).`, {
|
|
131
|
+
cause: result.error,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
if (result?.status !== 0) {
|
|
135
|
+
const code = npmErrorCode(result);
|
|
136
|
+
if (PERMISSION_CODES.has(code)) {
|
|
137
|
+
throw fail(
|
|
138
|
+
ERROR_CATEGORIES.CONFIG,
|
|
139
|
+
`npm cannot write its global install directory (${code}). Fix npm global permissions and try again.`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const category =
|
|
143
|
+
action === 'checking the registry' || NETWORK_CODES.has(code)
|
|
144
|
+
? ERROR_CATEGORIES.NETWORK
|
|
145
|
+
: ERROR_CATEGORIES.CONFIG;
|
|
146
|
+
throw fail(
|
|
147
|
+
category,
|
|
148
|
+
`npm failed while ${action}${code ? ` (${code})` : ''}. ` +
|
|
149
|
+
(action === 'checking the registry'
|
|
150
|
+
? 'Check the configured registry, authentication, proxy, and network connection.'
|
|
151
|
+
: `Run "npm install --global ${PACKAGE_NAME}@latest" manually for full npm diagnostics.`),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return String(result.stdout || '').trim();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function latestVersionFrom(output) {
|
|
158
|
+
let value;
|
|
159
|
+
try {
|
|
160
|
+
value = JSON.parse(output);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
throw fail(ERROR_CATEGORIES.NETWORK, 'npm returned invalid metadata for the latest version.', {
|
|
163
|
+
cause: error,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
if (typeof value !== 'string') {
|
|
167
|
+
throw fail(ERROR_CATEGORIES.NETWORK, 'npm returned invalid metadata for the latest version.');
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
parseVersion(value);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
throw fail(ERROR_CATEGORIES.NETWORK, `npm returned an invalid latest version: ${value}.`, {
|
|
173
|
+
cause: error,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function normalizedRealPath(path, platform) {
|
|
180
|
+
const value = realpathSync.native(path);
|
|
181
|
+
return platform === 'win32' ? value.toLowerCase() : value;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function assertActiveGlobalInstall(globalRoot, packageRoot, platform) {
|
|
185
|
+
const expectedPackageRoot = join(globalRoot, PACKAGE_NAME);
|
|
186
|
+
let expectedStat;
|
|
187
|
+
try {
|
|
188
|
+
expectedStat = lstatSync(expectedPackageRoot);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
throw fail(
|
|
191
|
+
ERROR_CATEGORIES.CONFIG,
|
|
192
|
+
`Cannot safely self-update this installation. The active npm global root does not contain ${PACKAGE_NAME}. ` +
|
|
193
|
+
`Reinstall it with "npm install --global ${PACKAGE_NAME}@latest".`,
|
|
194
|
+
{ cause: error },
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
if (expectedStat.isSymbolicLink()) {
|
|
198
|
+
throw fail(
|
|
199
|
+
ERROR_CATEGORIES.CONFIG,
|
|
200
|
+
'Cannot self-update an npm-linked source checkout. Update the checkout manually, or replace the link with a global npm installation.',
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
let activePath;
|
|
204
|
+
let currentPath;
|
|
205
|
+
try {
|
|
206
|
+
activePath = normalizedRealPath(expectedPackageRoot, platform);
|
|
207
|
+
currentPath = normalizedRealPath(packageRoot, platform);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
throw fail(ERROR_CATEGORIES.CONFIG, 'Cannot resolve the active aicommit installation.', {
|
|
210
|
+
cause: error,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
if (activePath !== currentPath) {
|
|
214
|
+
throw fail(
|
|
215
|
+
ERROR_CATEGORIES.CONFIG,
|
|
216
|
+
'Cannot safely self-update this installation because it belongs to a different Node.js or npm environment. ' +
|
|
217
|
+
`Activate the environment that installed it, or run "npm install --global ${PACKAGE_NAME}@latest" manually.`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function installedVersion(packageRoot) {
|
|
223
|
+
try {
|
|
224
|
+
const value = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8')).version;
|
|
225
|
+
parseVersion(value);
|
|
226
|
+
return value;
|
|
227
|
+
} catch (error) {
|
|
228
|
+
throw fail(
|
|
229
|
+
ERROR_CATEGORIES.INTERNAL,
|
|
230
|
+
'The npm update finished, but the installed aicommit version could not be verified.',
|
|
231
|
+
{ cause: error },
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function runUpdate(options = {}) {
|
|
237
|
+
const currentVersion = options.currentVersion || CURRENT_VERSION;
|
|
238
|
+
const packageRoot = options.packageRoot || PACKAGE_ROOT;
|
|
239
|
+
const platform = options.platform || process.platform;
|
|
240
|
+
const runner = options.runner || ((args) => runNpm(args, { platform }));
|
|
241
|
+
const log = options.log || console.log;
|
|
242
|
+
const debug = Boolean(options.debug);
|
|
243
|
+
|
|
244
|
+
parseVersion(currentVersion);
|
|
245
|
+
log('');
|
|
246
|
+
log(' ' + chalk.cyan.bold('Update aicommit'));
|
|
247
|
+
log(' ' + chalk.dim('─'.repeat(45)));
|
|
248
|
+
log(chalk.dim(' Checking the npm registry...'));
|
|
249
|
+
|
|
250
|
+
const latestVersion = latestVersionFrom(
|
|
251
|
+
runCheckedNpm(
|
|
252
|
+
['view', PACKAGE_NAME, 'dist-tags.latest', '--json'],
|
|
253
|
+
runner,
|
|
254
|
+
'checking the registry',
|
|
255
|
+
debug,
|
|
256
|
+
log,
|
|
257
|
+
),
|
|
258
|
+
);
|
|
259
|
+
log(` Current version: ${chalk.bold(currentVersion)}`);
|
|
260
|
+
log(` Latest version: ${chalk.bold(latestVersion)}`);
|
|
261
|
+
|
|
262
|
+
const comparison = compareVersions(currentVersion, latestVersion);
|
|
263
|
+
if (comparison >= 0) {
|
|
264
|
+
const newer = comparison > 0;
|
|
265
|
+
const message = newer
|
|
266
|
+
? `aicommit ${currentVersion} is newer than npm latest ${latestVersion}.`
|
|
267
|
+
: `aicommit ${currentVersion} is already up to date.`;
|
|
268
|
+
log('\n ' + chalk.green('✓') + ` ${message}\n`);
|
|
269
|
+
return {
|
|
270
|
+
message,
|
|
271
|
+
warnings: [],
|
|
272
|
+
exitReason: newer ? 'newer_than_latest' : 'up_to_date',
|
|
273
|
+
committed: false,
|
|
274
|
+
data: {
|
|
275
|
+
packageName: PACKAGE_NAME,
|
|
276
|
+
currentVersion,
|
|
277
|
+
latestVersion,
|
|
278
|
+
installedVersion: currentVersion,
|
|
279
|
+
updated: false,
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const globalRoot = runCheckedNpm(
|
|
285
|
+
['root', '--global'],
|
|
286
|
+
runner,
|
|
287
|
+
'locating the global installation',
|
|
288
|
+
debug,
|
|
289
|
+
log,
|
|
290
|
+
);
|
|
291
|
+
assertActiveGlobalInstall(globalRoot, packageRoot, platform);
|
|
292
|
+
|
|
293
|
+
log(chalk.dim(`\n Installing ${PACKAGE_NAME}@${latestVersion}...`));
|
|
294
|
+
runCheckedNpm(
|
|
295
|
+
['install', '--global', `${PACKAGE_NAME}@${latestVersion}`, '--no-audit', '--no-fund'],
|
|
296
|
+
runner,
|
|
297
|
+
'installing the update',
|
|
298
|
+
debug,
|
|
299
|
+
log,
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
const verifiedVersion = installedVersion(packageRoot);
|
|
303
|
+
if (verifiedVersion !== latestVersion) {
|
|
304
|
+
throw fail(
|
|
305
|
+
ERROR_CATEGORIES.INTERNAL,
|
|
306
|
+
`npm completed, but aicommit is still at ${verifiedVersion} instead of ${latestVersion}. ` +
|
|
307
|
+
'Check which aicommit executable is first in PATH.',
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const message = `Updated aicommit from ${currentVersion} to ${verifiedVersion}.`;
|
|
312
|
+
log('\n ' + chalk.green('✓') + ` ${message}\n`);
|
|
313
|
+
return {
|
|
314
|
+
message,
|
|
315
|
+
warnings: [],
|
|
316
|
+
exitReason: 'updated',
|
|
317
|
+
committed: false,
|
|
318
|
+
data: {
|
|
319
|
+
packageName: PACKAGE_NAME,
|
|
320
|
+
currentVersion,
|
|
321
|
+
latestVersion,
|
|
322
|
+
installedVersion: verifiedVersion,
|
|
323
|
+
updated: true,
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
}
|