@hifullmoon/aicommit 2.2.2 → 2.3.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/CHANGELOG.md +21 -1
- package/README.md +10 -1
- package/README.zh-CN.md +10 -1
- package/docs/distribution.md +8 -1
- package/docs/troubleshooting.md +2 -0
- package/package.json +1 -1
- package/src/api.js +14 -8
- package/src/cli.js +31 -5
- package/src/completion.js +3 -0
- package/src/main.js +6 -2
- package/src/setup.js +12 -2
- package/src/split.js +1 -1
- package/src/update.js +326 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@ This file lists notable user-facing changes. Internal refactors, test-only chang
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.3.0] - 2026-09-05
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added `aicommit update` for verified self-updates of regular npm-global installations, with exact-version installation, JSON output, and safeguards against updating source links or the wrong Node.js environment.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Moved the cancel action to the end of sensitive-data confirmation menus to keep send choices together.
|
|
16
|
+
|
|
17
|
+
## [2.2.3] - 2026-09-01
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Prevented accepted provider requests from being replayed after response-body interruptions, avoiding duplicate generations and potential duplicate billing.
|
|
22
|
+
- Honored provider retry timing safely by failing clearly when `Retry-After` exceeds the configured maximum delay instead of retrying too early.
|
|
23
|
+
- Stopped setup from replacing valid JSON configurations with unsupported schemas or invalid structures.
|
|
24
|
+
|
|
7
25
|
## [2.2.2] - 2026-08-31
|
|
8
26
|
|
|
9
27
|
### Added
|
|
@@ -170,7 +188,9 @@ This file lists notable user-facing changes. Internal refactors, test-only chang
|
|
|
170
188
|
- Added file-level split planning and execution with Git-state concurrency checks.
|
|
171
189
|
- Added provider presets and user/project configuration boundaries.
|
|
172
190
|
|
|
173
|
-
[Unreleased]: https://github.com/hi-fullmoon/AICommit/compare/v2.
|
|
191
|
+
[Unreleased]: https://github.com/hi-fullmoon/AICommit/compare/v2.3.0...HEAD
|
|
192
|
+
[2.3.0]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.3.0
|
|
193
|
+
[2.2.3]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.2.3
|
|
174
194
|
[2.2.2]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.2.2
|
|
175
195
|
[2.2.1]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.2.1
|
|
176
196
|
[2.2.0]: https://github.com/hi-fullmoon/AICommit/releases/tag/v2.2.0
|
package/README.md
CHANGED
|
@@ -32,6 +32,14 @@ npm install --global @hifullmoon/aicommit
|
|
|
32
32
|
|
|
33
33
|
Requires Node.js >= 18.
|
|
34
34
|
|
|
35
|
+
Update an npm-global installation from the configured registry:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
aicommit update
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The command resolves npm's `latest` dist-tag, installs that exact version, and verifies the installed manifest. It refuses to modify a source checkout, an `npm link`, or a package owned by a different active Node.js/npm environment; use the manual upgrade command from the distribution guide in those cases.
|
|
42
|
+
|
|
35
43
|
See the bilingual [installation, upgrade, signature-verification, and rollback guide](docs/distribution.md). The npm package has an automated installation smoke test.
|
|
36
44
|
|
|
37
45
|
To install a source checkout instead, run `npm install --global .` from the repository root.
|
|
@@ -287,6 +295,7 @@ Project-level configuration is treated as untrusted: it cannot change the endpoi
|
|
|
287
295
|
|
|
288
296
|
```bash
|
|
289
297
|
aicommit setup # interactive configuration wizard
|
|
298
|
+
aicommit update # update the global npm installation to latest
|
|
290
299
|
aicommit doctor # diagnose runtime, config, credentials, and connectivity
|
|
291
300
|
aicommit config show # show the effective config with secrets redacted
|
|
292
301
|
aicommit config validate # validate config without resolving credentials
|
|
@@ -376,7 +385,7 @@ Verify the registration with `whence -w _aicommit`; it should print `_aicommit:
|
|
|
376
385
|
|
|
377
386
|
### Machine-readable output
|
|
378
387
|
|
|
379
|
-
Use `--output=json` for scripts and CI. Commit and split flows also require `--yes`, preventing a machine consumer from hanging on an interactive prompt. stdout contains exactly one JSON object; progress, debug details, and diagnostics go to stderr. `doctor --output=json`
|
|
388
|
+
Use `--output=json` for scripts and CI. Commit and split flows also require `--yes`, preventing a machine consumer from hanging on an interactive prompt. stdout contains exactly one JSON object; progress, debug details, and diagnostics go to stderr. `doctor --output=json` and `update --output=json` do not require `--yes`.
|
|
380
389
|
|
|
381
390
|
```json
|
|
382
391
|
{
|
package/README.zh-CN.md
CHANGED
|
@@ -34,6 +34,14 @@ npm install --global @hifullmoon/aicommit
|
|
|
34
34
|
|
|
35
35
|
需要 Node.js >= 18。
|
|
36
36
|
|
|
37
|
+
从当前配置的 registry 更新 npm 全局安装:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
aicommit update
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
该命令会解析 npm 的 `latest` dist-tag,安装对应的精确版本,并校验安装后的 manifest。源码检出、`npm link`,或属于另一个 Node.js/npm 环境的安装会被拒绝;这些情况请使用分发指南中的手动升级命令。
|
|
44
|
+
|
|
37
45
|
安装、升级、签名校验与回滚请参阅双语[分发指南](docs/distribution.md)。npm package 带有自动化安装冒烟测试。
|
|
38
46
|
|
|
39
47
|
如需直接安装源码检出版本,请在仓库根目录运行 `npm install --global .`。
|
|
@@ -289,6 +297,7 @@ AICommit 不会主动发送无关的仓库文件、历史提交正文、环境
|
|
|
289
297
|
|
|
290
298
|
```bash
|
|
291
299
|
aicommit setup # 交互式配置向导
|
|
300
|
+
aicommit update # 将 npm 全局安装更新到最新版
|
|
292
301
|
aicommit doctor # 诊断运行时、配置、凭据和连接
|
|
293
302
|
aicommit config show # 显示脱敏后的有效配置
|
|
294
303
|
aicommit config validate # 校验配置,但不解析凭据
|
|
@@ -378,7 +387,7 @@ exec zsh
|
|
|
378
387
|
|
|
379
388
|
### 机器可读输出
|
|
380
389
|
|
|
381
|
-
脚本和 CI 请使用 `--output=json`。提交和 split 流程还必须使用 `--yes`,避免机器消费者卡在交互提示上。stdout 只包含一个 JSON 对象;进度、调试信息和诊断输出会写入 stderr。`doctor --output=json` 不要求 `--yes`。
|
|
390
|
+
脚本和 CI 请使用 `--output=json`。提交和 split 流程还必须使用 `--yes`,避免机器消费者卡在交互提示上。stdout 只包含一个 JSON 对象;进度、调试信息和诊断输出会写入 stderr。`doctor --output=json` 和 `update --output=json` 不要求 `--yes`。
|
|
382
391
|
|
|
383
392
|
```json
|
|
384
393
|
{
|
package/docs/distribution.md
CHANGED
|
@@ -11,6 +11,9 @@ AICommit is distributed exclusively through npm.
|
|
|
11
11
|
npm install --global @hifullmoon/aicommit
|
|
12
12
|
|
|
13
13
|
# upgrade / 升级
|
|
14
|
+
aicommit update
|
|
15
|
+
|
|
16
|
+
# manual upgrade / 手动升级
|
|
14
17
|
npm install --global @hifullmoon/aicommit@latest
|
|
15
18
|
|
|
16
19
|
# pin or roll back / 固定或回滚
|
|
@@ -19,6 +22,10 @@ npm install --global @hifullmoon/aicommit@1.4.0
|
|
|
19
22
|
aicommit --version
|
|
20
23
|
```
|
|
21
24
|
|
|
25
|
+
`aicommit update` 使用当前 `PATH` 中的 npm 和它配置的 registry,解析 `latest` dist-tag 后安装精确版本,并校验安装后的 package manifest。它仅更新当前 npm 全局根目录中的普通安装;源码检出、`npm link`、`npx` 缓存和其他 Node.js/npm 环境中的安装会被拒绝,以免更新错误的可执行文件。此时请切换到安装 AICommit 的 Node.js 环境,或使用上面的手动命令。
|
|
26
|
+
|
|
27
|
+
`aicommit update` uses the npm on the current `PATH` and its configured registry. It resolves the `latest` dist-tag, installs that exact version, and verifies the installed package manifest. It only updates a regular installation in the active npm global root; source checkouts, `npm link`, `npx` caches, and installations owned by another Node.js/npm environment are rejected to avoid updating the wrong executable. Switch to the Node.js environment that installed AICommit, or use the manual command above.
|
|
28
|
+
|
|
22
29
|
发布工作流使用 npm Trusted Publishing,不保存长期 `NPM_TOKEN`。来自公开 GitHub 仓库的 OIDC 发布会自动携带 npm provenance。可使用当前 npm CLI 检查 registry signature 与 provenance:
|
|
23
30
|
|
|
24
31
|
The release workflow uses npm Trusted Publishing without a long-lived `NPM_TOKEN`. OIDC publishing from the public GitHub repository automatically includes npm provenance. Verify registry signatures and provenance with a current npm CLI:
|
|
@@ -26,7 +33,7 @@ The release workflow uses npm Trusted Publishing without a long-lived `NPM_TOKEN
|
|
|
26
33
|
```bash
|
|
27
34
|
workdir=$(mktemp -d)
|
|
28
35
|
cd "$workdir"
|
|
29
|
-
npm install --package-lock-only @hifullmoon/aicommit@2.
|
|
36
|
+
npm install --package-lock-only @hifullmoon/aicommit@2.3.0
|
|
30
37
|
npm audit signatures
|
|
31
38
|
```
|
|
32
39
|
|
package/docs/troubleshooting.md
CHANGED
|
@@ -26,6 +26,8 @@ JSON 模式保证 stdout 只有一个机器对象,诊断进入 stderr。`error
|
|
|
26
26
|
| `split run --scope=all --yes` stops before API call / split 非交互在 API 前停止 | `sensitive_data` / `7` | Complete untracked scan found sensitive-looking data / 完整未跟踪扫描发现疑似敏感数据 | Review/stage intended files explicitly; do not bypass without checking the actual content |
|
|
27
27
|
| Commit aborts after generation / 生成后提交中止 | `concurrent_modification` / `8` | Index/worktree changed during the protected window / 受保护窗口中 index/worktree 被修改 | Review `git status`, restore the intended snapshot, and generate again |
|
|
28
28
|
| Split stopped after one or more commits / split 部分提交后停止 | reported Git failure | Hook, crash, SIGINT, or concurrent pending edit / hook、崩溃、中断或待处理文件变化 | Run `aicommit split resume`; if another Git workflow already replaced the transaction, use `aicommit split abort`(只删除恢复元数据,不改提交或工作区) |
|
|
29
|
+
| `aicommit update` refuses the installation / update 拒绝当前安装 | `config` / `2` | Source checkout, npm link, or another active Node/npm environment / 源码、npm link 或当前是另一套 Node/npm 环境 | Activate the Node environment that installed AICommit, or run `npm install --global @hifullmoon/aicommit@latest` manually |
|
|
30
|
+
| `aicommit update` cannot reach npm / update 无法访问 npm | `network` / `4` | Registry authentication, proxy, DNS, or network failure / registry 鉴权、代理、DNS 或网络失败 | Check `npm config get registry` and npm authentication/proxy settings, then retry |
|
|
29
31
|
| npm provenance is absent or invalid / npm provenance 缺失或失败 | npm audit failure | Old npm CLI, non-trusted release, or wrong version / npm 过旧、非可信发布或版本错误 | Upgrade npm; run `npm audit signatures`; install only a version linked to the official workflow |
|
|
30
32
|
|
|
31
33
|
If a failure remains, capture `aicommit doctor --output=json`, Node/Git versions, the error category, and redacted config sources. Never attach a diff, commit message, config file, API key, reasoning trace, or credential-helper output to a public issue.
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -136,18 +136,24 @@ async function fetchWithRetry(apiUrl, init, timeout, configuredPolicy, consume)
|
|
|
136
136
|
} catch (err) {
|
|
137
137
|
const wrappedTimeout = timeoutError(err, timeout);
|
|
138
138
|
if (wrappedTimeout) throw wrappedTimeout;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
// Once the provider has accepted a generation request, replaying it is
|
|
140
|
+
// unsafe: the first request may already have completed and been billed
|
|
141
|
+
// even though its response body was interrupted locally.
|
|
142
|
+
throw err;
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
if (RETRYABLE_STATUS.has(response.status) && attempt < policy.maxAttempts) {
|
|
146
146
|
const requestedDelay = retryAfterMs(response.headers.get('retry-after'), policy.now);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
if (requestedDelay !== null && requestedDelay > policy.maxDelayMs) {
|
|
148
|
+
await response.body?.cancel().catch(() => {});
|
|
149
|
+
throw new Error(
|
|
150
|
+
`HTTP ${response.status}: provider requested a retry after ${Math.ceil(
|
|
151
|
+
requestedDelay / 1000,
|
|
152
|
+
)}s, exceeding the configured retry.maxDelayMs limit.`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const delay =
|
|
156
|
+
requestedDelay ?? Math.min(policy.baseDelayMs * 2 ** (attempt - 1), policy.maxDelayMs);
|
|
151
157
|
await response.body?.cancel().catch(() => {});
|
|
152
158
|
await policy.sleep(delay);
|
|
153
159
|
continue;
|
package/src/cli.js
CHANGED
|
@@ -15,10 +15,12 @@ function showHelp() {
|
|
|
15
15
|
${chalk.bold('Usage:')}
|
|
16
16
|
${chalk.dim('$')} aicommit [path] [options]
|
|
17
17
|
${chalk.dim('$')} aicommit setup
|
|
18
|
+
${chalk.dim('$')} aicommit update
|
|
18
19
|
${chalk.dim('$')} aicommit split [run|plan|apply|resume|abort] [options]
|
|
19
20
|
|
|
20
21
|
${chalk.bold('Everyday commands:')}
|
|
21
22
|
setup Interactive configuration wizard
|
|
23
|
+
update Update the global npm installation to latest
|
|
22
24
|
doctor Diagnose runtime, config, credentials, and connectivity
|
|
23
25
|
split Plan and create file-level logical commits
|
|
24
26
|
|
|
@@ -92,6 +94,7 @@ function parsedDefaults(overrides = {}) {
|
|
|
92
94
|
dryRun: false,
|
|
93
95
|
yes: false,
|
|
94
96
|
setup: false,
|
|
97
|
+
update: false,
|
|
95
98
|
doctor: false,
|
|
96
99
|
configAction: null,
|
|
97
100
|
policyAction: null,
|
|
@@ -132,7 +135,10 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
132
135
|
return parsedDefaults({ completionShell: shell });
|
|
133
136
|
}
|
|
134
137
|
|
|
135
|
-
|
|
138
|
+
const update = args[0] === 'update';
|
|
139
|
+
if (update) args = args.slice(1);
|
|
140
|
+
|
|
141
|
+
if (!update && args[0] === 'policy' && args[1] === 'template') {
|
|
136
142
|
if (args.length !== 2) {
|
|
137
143
|
throw fail(ERROR_CATEGORIES.CONFIG, 'policy template takes no arguments.');
|
|
138
144
|
}
|
|
@@ -140,7 +146,7 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
140
146
|
}
|
|
141
147
|
|
|
142
148
|
let policyAction = null;
|
|
143
|
-
if (args[0] === 'policy') {
|
|
149
|
+
if (!update && args[0] === 'policy') {
|
|
144
150
|
policyAction = args[1];
|
|
145
151
|
if (policyAction !== 'check') {
|
|
146
152
|
throw fail(ERROR_CATEGORIES.CONFIG, 'policy requires one action: template or check.');
|
|
@@ -149,7 +155,7 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
149
155
|
}
|
|
150
156
|
|
|
151
157
|
let configAction = null;
|
|
152
|
-
if (args[0] === 'config') {
|
|
158
|
+
if (!update && args[0] === 'config') {
|
|
153
159
|
configAction = args[1];
|
|
154
160
|
if (!['show', 'validate', 'path'].includes(configAction)) {
|
|
155
161
|
throw fail(ERROR_CATEGORIES.CONFIG, 'config requires one action: show, validate, or path.');
|
|
@@ -158,7 +164,7 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
158
164
|
}
|
|
159
165
|
|
|
160
166
|
let splitCommand = null;
|
|
161
|
-
if (args[0] === 'split') {
|
|
167
|
+
if (!update && args[0] === 'split') {
|
|
162
168
|
const requestedAction = args[1];
|
|
163
169
|
const actions = ['run', 'plan', 'apply', 'resume', 'abort'];
|
|
164
170
|
if (!requestedAction || requestedAction.startsWith('-')) {
|
|
@@ -178,7 +184,7 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
178
184
|
}
|
|
179
185
|
}
|
|
180
186
|
|
|
181
|
-
const doctor = args[0] === 'doctor';
|
|
187
|
+
const doctor = !update && args[0] === 'doctor';
|
|
182
188
|
if (doctor) args = args.slice(1);
|
|
183
189
|
|
|
184
190
|
let targetPath = null;
|
|
@@ -458,6 +464,25 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
458
464
|
'doctor accepts only --provider, --model, --output, and --debug options.',
|
|
459
465
|
);
|
|
460
466
|
}
|
|
467
|
+
if (
|
|
468
|
+
update &&
|
|
469
|
+
(targetPath ||
|
|
470
|
+
cliLang ||
|
|
471
|
+
cliProvider ||
|
|
472
|
+
cliModel ||
|
|
473
|
+
cliReasoning ||
|
|
474
|
+
split ||
|
|
475
|
+
splitCommand ||
|
|
476
|
+
splitPlanFile ||
|
|
477
|
+
dryRun ||
|
|
478
|
+
yes ||
|
|
479
|
+
configAction ||
|
|
480
|
+
policyAction ||
|
|
481
|
+
policyMessageFile ||
|
|
482
|
+
policyRange)
|
|
483
|
+
) {
|
|
484
|
+
throw fail(ERROR_CATEGORIES.CONFIG, 'update accepts only --output and --debug options.');
|
|
485
|
+
}
|
|
461
486
|
|
|
462
487
|
return {
|
|
463
488
|
targetPath,
|
|
@@ -473,6 +498,7 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
473
498
|
dryRun,
|
|
474
499
|
yes,
|
|
475
500
|
setup,
|
|
501
|
+
update,
|
|
476
502
|
doctor,
|
|
477
503
|
configAction,
|
|
478
504
|
policyAction,
|
package/src/completion.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const TOP_LEVEL = [
|
|
2
2
|
'setup',
|
|
3
|
+
'update',
|
|
3
4
|
'doctor',
|
|
4
5
|
'config',
|
|
5
6
|
'policy',
|
|
@@ -51,6 +52,7 @@ _aicommit() {
|
|
|
51
52
|
typeset -A opt_args
|
|
52
53
|
commands=(
|
|
53
54
|
'setup:interactive configuration wizard'
|
|
55
|
+
'update:update the global npm installation'
|
|
54
56
|
'doctor:diagnose configuration and connectivity'
|
|
55
57
|
'config:inspect or validate configuration'
|
|
56
58
|
'policy:print or enforce a repository team policy'
|
|
@@ -94,6 +96,7 @@ _aicommit "$@"
|
|
|
94
96
|
const FISH = `# fish completion for aicommit
|
|
95
97
|
complete -c aicommit -f
|
|
96
98
|
complete -c aicommit -n '__fish_use_subcommand' -a setup -d 'Interactive configuration wizard'
|
|
99
|
+
complete -c aicommit -n '__fish_use_subcommand' -a update -d 'Update the global npm installation'
|
|
97
100
|
complete -c aicommit -n '__fish_use_subcommand' -a doctor -d 'Diagnose configuration and connectivity'
|
|
98
101
|
complete -c aicommit -n '__fish_use_subcommand' -a config -d 'Inspect or validate configuration'
|
|
99
102
|
complete -c aicommit -n '__fish_use_subcommand' -a policy -d 'Print or enforce a repository team policy'
|
package/src/main.js
CHANGED
|
@@ -50,6 +50,7 @@ import { runDoctor } from './doctor.js';
|
|
|
50
50
|
import { runConfigCommand } from './config-command.js';
|
|
51
51
|
import { generateCompletion } from './completion.js';
|
|
52
52
|
import { runPolicyCommand } from './policy-command.js';
|
|
53
|
+
import { runUpdate } from './update.js';
|
|
53
54
|
import {
|
|
54
55
|
applyCommitlintPolicy,
|
|
55
56
|
collectRepositoryContext,
|
|
@@ -97,6 +98,7 @@ async function runMain() {
|
|
|
97
98
|
dryRun,
|
|
98
99
|
yes,
|
|
99
100
|
setup,
|
|
101
|
+
update,
|
|
100
102
|
doctor,
|
|
101
103
|
configAction,
|
|
102
104
|
policyAction,
|
|
@@ -113,10 +115,12 @@ async function runMain() {
|
|
|
113
115
|
return { exitReason: 'completion' };
|
|
114
116
|
}
|
|
115
117
|
const machineOutput = output === 'json';
|
|
116
|
-
if (machineOutput && !yes && !doctor && !configAction && !policyAction) {
|
|
118
|
+
if (machineOutput && !yes && !doctor && !configAction && !policyAction && !update) {
|
|
117
119
|
throw fail(ERROR_CATEGORIES.CONFIG, '--output=json requires --yes for commit and split flows.');
|
|
118
120
|
}
|
|
119
121
|
|
|
122
|
+
if (update) return runUpdate({ machineOutput, debug });
|
|
123
|
+
|
|
120
124
|
// The setup wizard is a standalone flow — no git repo, diff, or loaded
|
|
121
125
|
// config required.
|
|
122
126
|
if (setup) {
|
|
@@ -513,12 +517,12 @@ async function runMain() {
|
|
|
513
517
|
description:
|
|
514
518
|
'Omit sensitive files/private keys and redact detected credential values',
|
|
515
519
|
},
|
|
516
|
-
{ name: 'Cancel', value: 'cancel', description: 'Do not send repository content' },
|
|
517
520
|
{
|
|
518
521
|
name: 'Send original diff',
|
|
519
522
|
value: 'original',
|
|
520
523
|
description: 'Send the unredacted content to the configured provider',
|
|
521
524
|
},
|
|
525
|
+
{ name: 'Cancel', value: 'cancel', description: 'Do not send repository content' },
|
|
522
526
|
],
|
|
523
527
|
});
|
|
524
528
|
if (sensitiveAction === 'cancel') {
|
package/src/setup.js
CHANGED
|
@@ -74,9 +74,9 @@ export function mergeSetupConfig(existing, { providerName, entry, language }) {
|
|
|
74
74
|
|
|
75
75
|
async function readExistingConfig(path) {
|
|
76
76
|
if (!(await fileExists(path))) return {};
|
|
77
|
+
let parsed;
|
|
77
78
|
try {
|
|
78
|
-
|
|
79
|
-
return validateUserConfig(parsed);
|
|
79
|
+
parsed = JSON.parse(await readFile(path, 'utf-8'));
|
|
80
80
|
} catch (err) {
|
|
81
81
|
const backup = `${path}.invalid-${Date.now()}.bak`;
|
|
82
82
|
await copyFile(path, backup);
|
|
@@ -84,6 +84,16 @@ async function readExistingConfig(path) {
|
|
|
84
84
|
console.log(chalk.dim(` The original file was preserved at ${backup}.`));
|
|
85
85
|
return {};
|
|
86
86
|
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
return validateUserConfig(parsed);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Existing config ${path} is valid JSON but has an unsupported structure: ${err.message} ` +
|
|
93
|
+
'Fix it or move it aside before running setup.',
|
|
94
|
+
{ cause: err },
|
|
95
|
+
);
|
|
96
|
+
}
|
|
87
97
|
}
|
|
88
98
|
|
|
89
99
|
async function writeConfigAtomic(path, value) {
|
package/src/split.js
CHANGED
|
@@ -1518,12 +1518,12 @@ export async function splitFlow(
|
|
|
1518
1518
|
description:
|
|
1519
1519
|
'Protect model input only; sensitive files remain in the reviewed commit plan',
|
|
1520
1520
|
},
|
|
1521
|
-
{ name: 'Cancel', value: 'cancel', description: 'Do not send repository content' },
|
|
1522
1521
|
{
|
|
1523
1522
|
name: 'Send original diff',
|
|
1524
1523
|
value: 'original',
|
|
1525
1524
|
description: 'Send the unredacted content to the configured provider',
|
|
1526
1525
|
},
|
|
1526
|
+
{ name: 'Cancel', value: 'cancel', description: 'Do not send repository content' },
|
|
1527
1527
|
],
|
|
1528
1528
|
});
|
|
1529
1529
|
if (sensitiveAction === 'cancel') {
|
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
|
+
}
|