@hifullmoon/aicommit 2.0.0 → 2.1.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 +0 -10
- package/CHANGELOG.md +24 -1
- package/README.md +11 -36
- package/README.zh-CN.md +11 -36
- package/SECURITY.md +0 -1
- package/bin/aicommit.js +0 -18
- package/docs/distribution.md +1 -1
- package/docs/privacy.md +18 -20
- package/docs/provider-compatibility.md +13 -25
- package/docs/team-policy.md +2 -2
- package/docs/troubleshooting.md +16 -19
- package/package.json +1 -1
- package/src/api.js +3 -20
- package/src/cli.js +16 -130
- package/src/completion.js +1 -16
- package/src/config-command.js +24 -6
- package/src/config-paths.js +31 -0
- package/src/config.js +22 -42
- package/src/doctor.js +1 -12
- package/src/git.js +1 -1
- package/src/main.js +2 -26
- package/src/provider-presets.js +5 -95
- package/src/providers.js +5 -6
- package/src/setup.js +18 -4
- package/src/split.js +4 -119
- package/src/utils.js +1 -1
- package/docs/examples/extension/aicommit-extension.json +0 -9
- package/docs/examples/extension/index.mjs +0 -32
- package/docs/extensions.md +0 -93
- package/docs/provider-presets.md +0 -113
- package/schemas/aicommit-extension.schema.json +0 -27
- package/src/extension-runner.mjs +0 -36
- package/src/extensions.js +0 -426
- package/src/metrics.js +0 -375
- package/src/preset-command.js +0 -91
package/docs/troubleshooting.md
CHANGED
|
@@ -15,22 +15,19 @@ JSON mode keeps one machine object on stdout and diagnostics on stderr. The `err
|
|
|
15
15
|
|
|
16
16
|
JSON 模式保证 stdout 只有一个机器对象,诊断进入 stderr。`error.category` 与进程退出码可稳定用于自动化。
|
|
17
17
|
|
|
18
|
-
| Symptom / 症状 | Category / exit | Likely cause / 常见原因
|
|
19
|
-
| ------------------------------------------------------------------------------- | ------------------------------- |
|
|
20
|
-
| Config fails before Git/provider access / 在 Git/provider 前配置失败 | `config` / `2` | Malformed JSON, invalid URL, unsupported option, missing credential / JSON 损坏、URL 或选项无效、凭据缺失
|
|
21
|
-
| Project settings are ignored / 项目设置被忽略 | warning | Repository config tried to set endpoint, credential,
|
|
22
|
-
| Not a Git repository, conflict, empty index / 非仓库、冲突或 index 为空 | `git_state` / `3` | Wrong directory or unsafe Git state / 目录错误或 Git 状态不安全
|
|
23
|
-
| DNS, connection, or timeout / DNS、连接或超时 | `network` / `4` | Endpoint unavailable, proxy/TLS issue, budget too short / endpoint 不可达、代理/TLS、超时过短
|
|
24
|
-
| HTTP authentication/rate/parameter failure / 鉴权、限流或参数失败 | `provider` / `5` | Wrong key/model/body; non-retryable 4xx / key、model、body 错误
|
|
25
|
-
| Empty or malformed model reply / 空或畸形回复 | `response_format` / `6` | Unsupported response dialect, token limit, policy
|
|
26
|
-
| `split run --scope=all --yes` stops before API call / split 非交互在 API 前停止 | `sensitive_data` / `7` | Complete untracked scan found sensitive-looking data / 完整未跟踪扫描发现疑似敏感数据
|
|
27
|
-
| Commit aborts after generation / 生成后提交中止 | `concurrent_modification` / `8` | Index/worktree changed during the protected window / 受保护窗口中 index/worktree 被修改
|
|
28
|
-
| Split stopped after one or more commits / split 部分提交后停止 | reported Git failure | Hook, crash, SIGINT, or concurrent pending edit / hook、崩溃、中断或待处理文件变化
|
|
29
|
-
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
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, extension source containing secrets, or credential-helper output to a public issue.
|
|
35
|
-
|
|
36
|
-
若问题仍未解决,请记录 `aicommit doctor --output=json`、Node/Git 版本、错误分类与脱敏后的配置来源。不要在公开 issue 中附加 diff、commit message、配置文件、API key、reasoning、含秘密的扩展源码或 credential-helper 输出。
|
|
18
|
+
| Symptom / 症状 | Category / exit | Likely cause / 常见原因 | Check and recovery / 检查与恢复 |
|
|
19
|
+
| ------------------------------------------------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
20
|
+
| Config fails before Git/provider access / 在 Git/provider 前配置失败 | `config` / `2` | Malformed JSON, invalid URL, unsupported option, missing credential / JSON 损坏、URL 或选项无效、凭据缺失 | `aicommit config path`; `aicommit config validate`; fix only the user-owned file shown |
|
|
21
|
+
| Project settings are ignored / 项目设置被忽略 | warning | Repository config tried to set endpoint, credential, reasoning, or raise a ceiling / 仓库配置尝试设置连接、凭据、推理或提高预算 | Move trusted connection settings to `~/.aicommit/config.json`; keep team rules in `.aicommit.policy.json` |
|
|
22
|
+
| Not a Git repository, conflict, empty index / 非仓库、冲突或 index 为空 | `git_state` / `3` | Wrong directory or unsafe Git state / 目录错误或 Git 状态不安全 | `git status`; `aicommit /absolute/repo/path`; resolve conflicts before retrying |
|
|
23
|
+
| DNS, connection, or timeout / DNS、连接或超时 | `network` / `4` | Endpoint unavailable, proxy/TLS issue, budget too short / endpoint 不可达、代理/TLS、超时过短 | `aicommit doctor -p NAME`; verify HTTPS URL; raise user-owned `timeoutMs` only if expected |
|
|
24
|
+
| HTTP authentication/rate/parameter failure / 鉴权、限流或参数失败 | `provider` / `5` | Wrong key/model/body; non-retryable 4xx / key、model、body 错误 | Check `apiKeyEnv`, model and compatibility table; authentication is never retried automatically |
|
|
25
|
+
| Empty or malformed model reply / 空或畸形回复 | `response_format` / `6` | Unsupported response dialect, token limit, or policy validation failure / 响应方言、token 限制或 policy 校验失败 | Raise `maxTokens`, choose the matching adapter, inspect validator issue code; one correction is already attempted |
|
|
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
|
+
| 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
|
+
| 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
|
+
| 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
|
+
|
|
31
|
+
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.
|
|
32
|
+
|
|
33
|
+
若问题仍未解决,请记录 `aicommit doctor --output=json`、Node/Git 版本、错误分类与脱敏后的配置来源。不要在公开 issue 中附加 diff、commit message、配置文件、API key、reasoning 或 credential-helper 输出。
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
validateCommitCandidate,
|
|
9
9
|
} from './policy.js';
|
|
10
10
|
import { encodeUntrustedData } from './trust.js';
|
|
11
|
-
import { extensionHostFor, resolveProviderAdapter } from './extensions.js';
|
|
12
11
|
|
|
13
12
|
// Default per-request timeout; overridable via the "timeoutMs" config key.
|
|
14
13
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
@@ -167,7 +166,7 @@ async function fetchWithRetry(apiUrl, init, timeout, configuredPolicy, consume)
|
|
|
167
166
|
export async function requestGeneration(config, request) {
|
|
168
167
|
secureEndpoint(config.apiUrl);
|
|
169
168
|
const timeout = config.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
170
|
-
const adapter =
|
|
169
|
+
const adapter = getProviderAdapter(config);
|
|
171
170
|
const payload = await adapter.buildRequest({
|
|
172
171
|
messages: request.messages,
|
|
173
172
|
temperature: request.temperature,
|
|
@@ -370,7 +369,7 @@ function hitTokenLimit(data) {
|
|
|
370
369
|
// happened. Disable it only for providers where we know the switch is valid;
|
|
371
370
|
// unknown compatible endpoints keep their configured behavior.
|
|
372
371
|
async function reasoningForFollowUp(config) {
|
|
373
|
-
const adapter =
|
|
372
|
+
const adapter = getProviderAdapter(config);
|
|
374
373
|
return adapter.reasoningForFollowUp(config.reasoning);
|
|
375
374
|
}
|
|
376
375
|
|
|
@@ -660,23 +659,7 @@ export async function generateCommitMessage(
|
|
|
660
659
|
// Validate against the versioned policy and give the provider exactly one
|
|
661
660
|
// cheap correction attempt. The diff is never re-sent: the prior reply plus
|
|
662
661
|
// concrete violations are sufficient to repair formatting and constraints.
|
|
663
|
-
const validate =
|
|
664
|
-
const builtIn = validateCommitCandidate(candidate, { policy, diff });
|
|
665
|
-
const host = extensionHostFor(config);
|
|
666
|
-
if (!host) return builtIn;
|
|
667
|
-
const extensionIssues = await host.validateMessage(candidate, policy);
|
|
668
|
-
const issues = [...builtIn.issues, ...extensionIssues];
|
|
669
|
-
const errors = issues.filter((item) => item.severity === 'error');
|
|
670
|
-
const warnings = issues.filter((item) => item.severity === 'warning');
|
|
671
|
-
return {
|
|
672
|
-
...builtIn,
|
|
673
|
-
valid: errors.length === 0,
|
|
674
|
-
needsCorrection: errors.length > 0,
|
|
675
|
-
issues,
|
|
676
|
-
errors,
|
|
677
|
-
warnings,
|
|
678
|
-
};
|
|
679
|
-
};
|
|
662
|
+
const validate = (candidate) => validateCommitCandidate(candidate, { policy, diff });
|
|
680
663
|
|
|
681
664
|
let validation = await validate(message);
|
|
682
665
|
if (message.trim() && validation.needsCorrection) {
|
package/src/cli.js
CHANGED
|
@@ -14,28 +14,24 @@ function showHelp() {
|
|
|
14
14
|
${chalk.bold('Usage:')}
|
|
15
15
|
${chalk.dim('$')} aicommit [path] [options]
|
|
16
16
|
${chalk.dim('$')} aicommit setup
|
|
17
|
-
${chalk.dim('$')} aicommit config <show|validate|path> [options]
|
|
18
|
-
${chalk.dim('$')} aicommit policy <template|check> [options]
|
|
19
|
-
${chalk.dim('$')} aicommit preset <show|validate|path|install|rollback> [options]
|
|
20
|
-
${chalk.dim('$')} aicommit completion <bash|zsh|fish>
|
|
21
17
|
${chalk.dim('$')} aicommit split [run|plan|apply|resume|abort] [options]
|
|
22
18
|
|
|
23
|
-
${chalk.bold('
|
|
19
|
+
${chalk.bold('Everyday commands:')}
|
|
24
20
|
setup Interactive configuration wizard
|
|
25
21
|
doctor Diagnose runtime, config, credentials, and connectivity
|
|
22
|
+
split Plan and create file-level logical commits
|
|
23
|
+
|
|
24
|
+
${chalk.bold('Advanced commands:')}
|
|
26
25
|
config show Print the effective configuration with secrets redacted
|
|
27
26
|
config validate Parse, merge, and validate configuration without reading credentials
|
|
28
27
|
config path Print user, project, and team-policy paths
|
|
29
28
|
policy template Print a safe repository team-policy template
|
|
30
29
|
policy check Validate a message file or Git range with the effective team policy
|
|
31
|
-
preset Inspect, validate, install, or roll back provider preset manifests
|
|
32
30
|
completion Generate Bash, Zsh, or Fish completion on stdout
|
|
33
|
-
split, split run Plan and create logical commits
|
|
34
31
|
split plan Generate and export a fingerprinted JSON plan
|
|
35
32
|
split apply Validate and apply an exported JSON plan
|
|
36
33
|
split resume Resume the repository's unfinished transaction
|
|
37
34
|
split abort Discard recovery metadata; keep commits and changes
|
|
38
|
-
stats [action] Show quality/cost trends; show, clear, enable, disable
|
|
39
35
|
|
|
40
36
|
${chalk.bold('Arguments:')}
|
|
41
37
|
path Target directory (default: current directory)
|
|
@@ -46,7 +42,6 @@ function showHelp() {
|
|
|
46
42
|
-l, --lang=<zh|en> Commit message language (default: zh)
|
|
47
43
|
-p, --provider=<name> Use the named provider from config "providers"
|
|
48
44
|
-m, --model=<name> Use a named model from the selected provider
|
|
49
|
-
--split-hunks Experimental same-file hunk planning (default: off)
|
|
50
45
|
--scope=<scope> Scope for "split|split plan": staged, all
|
|
51
46
|
--file=<path> Split-plan artifact or commit-message file
|
|
52
47
|
--range=<revision> Git revision/range for "policy check" (default: HEAD)
|
|
@@ -58,35 +53,11 @@ function showHelp() {
|
|
|
58
53
|
--debug Print debug info (parsed args, final config, etc.)
|
|
59
54
|
|
|
60
55
|
${chalk.bold('Examples:')}
|
|
61
|
-
aicommit setup Run the interactive configuration wizard
|
|
62
|
-
aicommit doctor Check runtime, config, credentials, and connectivity
|
|
63
|
-
aicommit config show Show effective redacted configuration
|
|
64
|
-
aicommit config validate --output=json Validate configuration for CI
|
|
65
|
-
aicommit config path Show user and project config locations
|
|
66
|
-
aicommit policy template > .aicommit.policy.json Create a committable policy
|
|
67
|
-
aicommit policy check --file=.git/COMMIT_EDITMSG Validate a local commit message
|
|
68
|
-
aicommit policy check --range=origin/main..HEAD --output=json Validate a CI range
|
|
69
|
-
aicommit preset show Show the active versioned provider preset manifest
|
|
70
|
-
aicommit preset install --file=provider-presets.json Install a compatible manifest
|
|
71
|
-
aicommit preset rollback Restore the previously installed manifest
|
|
72
|
-
aicommit completion zsh > _aicommit Generate Zsh completion
|
|
73
|
-
aicommit stats Show local acceptance, quality, latency, and token trends
|
|
74
56
|
aicommit Commit changes in current directory (Chinese)
|
|
75
57
|
aicommit --lang=en Generate English commit message
|
|
76
|
-
aicommit -p deepseek -m chat Select a configured provider and model
|
|
77
|
-
aicommit split Choose staged/all scope, then plan logical commits
|
|
78
58
|
aicommit split --scope=staged Split only the reviewed index snapshot
|
|
79
|
-
aicommit split --scope=all --yes Split the complete working tree non-interactively
|
|
80
|
-
aicommit split --scope=staged --split-hunks Split eligible text hunks
|
|
81
|
-
aicommit split plan --scope=staged --file=.aicommit-plan.json --yes
|
|
82
|
-
aicommit split apply --file=.aicommit-plan.json --yes
|
|
83
|
-
aicommit split resume --yes Resume pending groups from the checkpoint
|
|
84
|
-
aicommit split abort --yes Discard a stale checkpoint without changing Git state
|
|
85
|
-
aicommit --reasoning=low Stream reasoning; Ctrl+O expands/collapses it
|
|
86
59
|
aicommit --dry-run Review a generated message without committing
|
|
87
60
|
aicommit --yes Commit already staged changes without prompts
|
|
88
|
-
aicommit --yes --output=json Emit one machine-readable result on stdout
|
|
89
|
-
aicommit /path/to Commit changes in the specified directory
|
|
90
61
|
`);
|
|
91
62
|
}
|
|
92
63
|
|
|
@@ -115,7 +86,6 @@ function parsedDefaults(overrides = {}) {
|
|
|
115
86
|
output: 'text',
|
|
116
87
|
debug: false,
|
|
117
88
|
split: null,
|
|
118
|
-
splitHunks: false,
|
|
119
89
|
splitCommand: null,
|
|
120
90
|
splitPlanFile: null,
|
|
121
91
|
dryRun: false,
|
|
@@ -126,10 +96,7 @@ function parsedDefaults(overrides = {}) {
|
|
|
126
96
|
policyAction: null,
|
|
127
97
|
policyMessageFile: null,
|
|
128
98
|
policyRange: null,
|
|
129
|
-
presetAction: null,
|
|
130
|
-
presetFile: null,
|
|
131
99
|
completionShell: null,
|
|
132
|
-
statsAction: null,
|
|
133
100
|
help: false,
|
|
134
101
|
version: false,
|
|
135
102
|
...overrides,
|
|
@@ -137,6 +104,13 @@ function parsedDefaults(overrides = {}) {
|
|
|
137
104
|
}
|
|
138
105
|
|
|
139
106
|
export function parseArgs(args = process.argv.slice(2)) {
|
|
107
|
+
if (args[0] === 'stats' || args[0] === 'preset') {
|
|
108
|
+
throw fail(
|
|
109
|
+
ERROR_CATEGORIES.CONFIG,
|
|
110
|
+
`The "${args[0]}" command was removed to keep aicommit focused on commit generation.`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
140
114
|
// "setup" is a standalone subcommand — it doesn't combine with the
|
|
141
115
|
// commit-flow options, so short-circuit before parsing them.
|
|
142
116
|
if (args[0] === 'setup') {
|
|
@@ -149,15 +123,6 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
149
123
|
return parsedDefaults({ setup: true });
|
|
150
124
|
}
|
|
151
125
|
|
|
152
|
-
if (args[0] === 'stats') {
|
|
153
|
-
const action = args[1] || 'show';
|
|
154
|
-
const allowed = ['show', 'clear', 'enable', 'disable'];
|
|
155
|
-
if (args.length > 2 || !allowed.includes(action)) {
|
|
156
|
-
throw fail(ERROR_CATEGORIES.CONFIG, `stats accepts one action: ${allowed.join(', ')}.`);
|
|
157
|
-
}
|
|
158
|
-
return parsedDefaults({ statsAction: action });
|
|
159
|
-
}
|
|
160
|
-
|
|
161
126
|
if (args[0] === 'completion') {
|
|
162
127
|
const shell = args[1];
|
|
163
128
|
if (args.length !== 2 || !['bash', 'zsh', 'fish'].includes(shell)) {
|
|
@@ -182,18 +147,6 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
182
147
|
args = args.slice(2);
|
|
183
148
|
}
|
|
184
149
|
|
|
185
|
-
let presetAction = null;
|
|
186
|
-
if (args[0] === 'preset') {
|
|
187
|
-
presetAction = args[1];
|
|
188
|
-
if (!['show', 'validate', 'path', 'install', 'rollback'].includes(presetAction)) {
|
|
189
|
-
throw fail(
|
|
190
|
-
ERROR_CATEGORIES.CONFIG,
|
|
191
|
-
'preset requires one action: show, validate, path, install, or rollback.',
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
args = args.slice(2);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
150
|
let configAction = null;
|
|
198
151
|
if (args[0] === 'config') {
|
|
199
152
|
configAction = args[1];
|
|
@@ -235,11 +188,9 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
235
188
|
let output = 'text';
|
|
236
189
|
let debug = false;
|
|
237
190
|
let split = null;
|
|
238
|
-
let splitHunks = false;
|
|
239
191
|
let splitPlanFile = null;
|
|
240
192
|
let policyMessageFile = null;
|
|
241
193
|
let policyRange = null;
|
|
242
|
-
let presetFile = null;
|
|
243
194
|
let splitScopeOption = false;
|
|
244
195
|
let dryRun = false;
|
|
245
196
|
let yes = false;
|
|
@@ -275,11 +226,6 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
275
226
|
continue;
|
|
276
227
|
}
|
|
277
228
|
|
|
278
|
-
if (arg === '--split-hunks') {
|
|
279
|
-
splitHunks = true;
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
229
|
if (arg === '--scope') {
|
|
284
230
|
splitScopeOption = true;
|
|
285
231
|
split = takeValue(args, i, arg, 'staged|all');
|
|
@@ -302,7 +248,6 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
302
248
|
if (arg === '--file') {
|
|
303
249
|
const value = takeValue(args, i, arg, 'path');
|
|
304
250
|
if (policyAction) policyMessageFile = value;
|
|
305
|
-
else if (presetAction) presetFile = value;
|
|
306
251
|
else splitPlanFile = value;
|
|
307
252
|
i++;
|
|
308
253
|
continue;
|
|
@@ -312,7 +257,6 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
312
257
|
const value = arg.slice('--file='.length);
|
|
313
258
|
if (!value) throw fail(ERROR_CATEGORIES.CONFIG, 'Missing value for --file.');
|
|
314
259
|
if (policyAction) policyMessageFile = value;
|
|
315
|
-
else if (presetAction) presetFile = value;
|
|
316
260
|
else splitPlanFile = value;
|
|
317
261
|
continue;
|
|
318
262
|
}
|
|
@@ -430,12 +374,6 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
430
374
|
if (!['text', 'json'].includes(output)) {
|
|
431
375
|
throw fail(ERROR_CATEGORIES.CONFIG, `Invalid output mode: "${output}". Use text or json.`);
|
|
432
376
|
}
|
|
433
|
-
if (splitHunks && !['run', 'plan'].includes(splitCommand)) {
|
|
434
|
-
throw fail(
|
|
435
|
-
ERROR_CATEGORIES.CONFIG,
|
|
436
|
-
'--split-hunks is only valid with "aicommit split run" or "aicommit split plan".',
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
377
|
if (splitCommand) {
|
|
440
378
|
if (['plan', 'apply'].includes(splitCommand) && !splitPlanFile) {
|
|
441
379
|
throw fail(ERROR_CATEGORIES.CONFIG, `split ${splitCommand} requires --file=<path>.`);
|
|
@@ -453,10 +391,10 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
453
391
|
}
|
|
454
392
|
if (splitCommand === 'plan') dryRun = true;
|
|
455
393
|
} else if (splitCommand === 'apply') {
|
|
456
|
-
if (split
|
|
394
|
+
if (split) {
|
|
457
395
|
throw fail(
|
|
458
396
|
ERROR_CATEGORIES.CONFIG,
|
|
459
|
-
'split apply reads its scope from the plan; do not pass --scope
|
|
397
|
+
'split apply reads its scope from the plan; do not pass --scope.',
|
|
460
398
|
);
|
|
461
399
|
}
|
|
462
400
|
if (cliProvider || cliModel || cliLang || cliReasoning || dryRun) {
|
|
@@ -466,16 +404,7 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
466
404
|
);
|
|
467
405
|
}
|
|
468
406
|
} else {
|
|
469
|
-
if (
|
|
470
|
-
splitPlanFile ||
|
|
471
|
-
split ||
|
|
472
|
-
splitHunks ||
|
|
473
|
-
cliProvider ||
|
|
474
|
-
cliModel ||
|
|
475
|
-
cliLang ||
|
|
476
|
-
cliReasoning ||
|
|
477
|
-
dryRun
|
|
478
|
-
) {
|
|
407
|
+
if (splitPlanFile || split || cliProvider || cliModel || cliLang || cliReasoning || dryRun) {
|
|
479
408
|
throw fail(
|
|
480
409
|
ERROR_CATEGORIES.CONFIG,
|
|
481
410
|
`split ${splitCommand} accepts only an optional path, --yes, --output, and --debug.`,
|
|
@@ -494,14 +423,7 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
494
423
|
}
|
|
495
424
|
if (
|
|
496
425
|
configAction &&
|
|
497
|
-
(cliLang ||
|
|
498
|
-
cliReasoning ||
|
|
499
|
-
split ||
|
|
500
|
-
splitHunks ||
|
|
501
|
-
splitCommand ||
|
|
502
|
-
splitPlanFile ||
|
|
503
|
-
dryRun ||
|
|
504
|
-
yes)
|
|
426
|
+
(cliLang || cliReasoning || split || splitCommand || splitPlanFile || dryRun || yes)
|
|
505
427
|
) {
|
|
506
428
|
throw fail(
|
|
507
429
|
ERROR_CATEGORIES.CONFIG,
|
|
@@ -513,15 +435,7 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
513
435
|
}
|
|
514
436
|
if (
|
|
515
437
|
policyAction &&
|
|
516
|
-
(cliProvider ||
|
|
517
|
-
cliModel ||
|
|
518
|
-
cliLang ||
|
|
519
|
-
cliReasoning ||
|
|
520
|
-
split ||
|
|
521
|
-
splitHunks ||
|
|
522
|
-
splitCommand ||
|
|
523
|
-
dryRun ||
|
|
524
|
-
yes)
|
|
438
|
+
(cliProvider || cliModel || cliLang || cliReasoning || split || splitCommand || dryRun || yes)
|
|
525
439
|
) {
|
|
526
440
|
throw fail(
|
|
527
441
|
ERROR_CATEGORIES.CONFIG,
|
|
@@ -534,30 +448,6 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
534
448
|
if (policyMessageFile && policyRange) {
|
|
535
449
|
throw fail(ERROR_CATEGORIES.CONFIG, 'policy check accepts either --file or --range, not both.');
|
|
536
450
|
}
|
|
537
|
-
if (
|
|
538
|
-
presetAction &&
|
|
539
|
-
(targetPath ||
|
|
540
|
-
cliProvider ||
|
|
541
|
-
cliModel ||
|
|
542
|
-
cliLang ||
|
|
543
|
-
cliReasoning ||
|
|
544
|
-
split ||
|
|
545
|
-
splitHunks ||
|
|
546
|
-
splitCommand ||
|
|
547
|
-
dryRun ||
|
|
548
|
-
yes)
|
|
549
|
-
) {
|
|
550
|
-
throw fail(
|
|
551
|
-
ERROR_CATEGORIES.CONFIG,
|
|
552
|
-
'preset accepts only --file (validate/install), --output, and --debug.',
|
|
553
|
-
);
|
|
554
|
-
}
|
|
555
|
-
if (presetAction === 'install' && !presetFile) {
|
|
556
|
-
throw fail(ERROR_CATEGORIES.CONFIG, 'preset install requires --file=<manifest>.');
|
|
557
|
-
}
|
|
558
|
-
if (presetFile && !['validate', 'install'].includes(presetAction)) {
|
|
559
|
-
throw fail(ERROR_CATEGORIES.CONFIG, '--file is only valid with preset validate or install.');
|
|
560
|
-
}
|
|
561
451
|
if (
|
|
562
452
|
doctor &&
|
|
563
453
|
(targetPath || cliLang || cliReasoning || split || splitPlanFile || dryRun || yes)
|
|
@@ -577,7 +467,6 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
577
467
|
output,
|
|
578
468
|
debug,
|
|
579
469
|
split,
|
|
580
|
-
splitHunks,
|
|
581
470
|
splitCommand,
|
|
582
471
|
splitPlanFile,
|
|
583
472
|
dryRun,
|
|
@@ -588,10 +477,7 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
588
477
|
policyAction,
|
|
589
478
|
policyMessageFile,
|
|
590
479
|
policyRange,
|
|
591
|
-
presetAction,
|
|
592
|
-
presetFile,
|
|
593
480
|
completionShell: null,
|
|
594
|
-
statsAction: null,
|
|
595
481
|
help: false,
|
|
596
482
|
version: false,
|
|
597
483
|
};
|
package/src/completion.js
CHANGED
|
@@ -3,16 +3,13 @@ const TOP_LEVEL = [
|
|
|
3
3
|
'doctor',
|
|
4
4
|
'config',
|
|
5
5
|
'policy',
|
|
6
|
-
'preset',
|
|
7
6
|
'completion',
|
|
8
7
|
'split',
|
|
9
|
-
'stats',
|
|
10
8
|
'--help',
|
|
11
9
|
'--version',
|
|
12
10
|
'--lang',
|
|
13
11
|
'--provider',
|
|
14
12
|
'--model',
|
|
15
|
-
'--split-hunks',
|
|
16
13
|
'--scope',
|
|
17
14
|
'--reasoning',
|
|
18
15
|
'--no-reasoning',
|
|
@@ -33,10 +30,8 @@ _aicommit() {
|
|
|
33
30
|
case "$command" in
|
|
34
31
|
config) COMPREPLY=( $(compgen -W "show validate path --provider --model --output --debug" -- "$cur") ); return ;;
|
|
35
32
|
policy) COMPREPLY=( $(compgen -W "template check --file --range --output --debug" -- "$cur") ); return ;;
|
|
36
|
-
preset) COMPREPLY=( $(compgen -W "show validate path install rollback --file --output --debug" -- "$cur") ); return ;;
|
|
37
33
|
completion) COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") ); return ;;
|
|
38
|
-
split) COMPREPLY=( $(compgen -W "run plan apply resume abort --scope --file --
|
|
39
|
-
stats) COMPREPLY=( $(compgen -W "show clear enable disable" -- "$cur") ); return ;;
|
|
34
|
+
split) COMPREPLY=( $(compgen -W "run plan apply resume abort --scope --file --yes --output --debug" -- "$cur") ); return ;;
|
|
40
35
|
esac
|
|
41
36
|
case "$prev" in
|
|
42
37
|
--lang|-l) COMPREPLY=( $(compgen -W "zh en" -- "$cur") ); return ;;
|
|
@@ -57,10 +52,8 @@ _aicommit() {
|
|
|
57
52
|
'doctor:diagnose configuration and connectivity'
|
|
58
53
|
'config:inspect or validate configuration'
|
|
59
54
|
'policy:print or enforce a repository team policy'
|
|
60
|
-
'preset:manage versioned provider preset manifests'
|
|
61
55
|
'completion:generate shell completion'
|
|
62
56
|
'split:run, plan, apply, resume, or abort split commits'
|
|
63
|
-
'stats:show local quality trends'
|
|
64
57
|
)
|
|
65
58
|
options=(
|
|
66
59
|
'--help[show help]'
|
|
@@ -68,7 +61,6 @@ _aicommit() {
|
|
|
68
61
|
'--lang=[commit language]:language:(zh en)'
|
|
69
62
|
'--provider=[provider name]:provider:'
|
|
70
63
|
'--model=[model name]:model:'
|
|
71
|
-
'--split-hunks[experimental same-file hunk splitting]'
|
|
72
64
|
'--scope=[split scope]:scope:(staged all)'
|
|
73
65
|
'--file=[split plan or commit-message file]:path:_files'
|
|
74
66
|
'--range=[Git range for policy check]:revision:'
|
|
@@ -85,10 +77,8 @@ _aicommit() {
|
|
|
85
77
|
case $words[2] in
|
|
86
78
|
config) _values 'config action' show validate path ;;
|
|
87
79
|
policy) _values 'policy action' template check ;;
|
|
88
|
-
preset) _values 'preset action' show validate path install rollback ;;
|
|
89
80
|
completion) _values 'shell' bash zsh fish ;;
|
|
90
81
|
split) _values 'split action' run plan apply resume abort ;;
|
|
91
|
-
stats) _values 'action' show clear enable disable ;;
|
|
92
82
|
*) _describe 'command' commands ;;
|
|
93
83
|
esac
|
|
94
84
|
;;
|
|
@@ -103,22 +93,17 @@ complete -c aicommit -n '__fish_use_subcommand' -a setup -d 'Interactive configu
|
|
|
103
93
|
complete -c aicommit -n '__fish_use_subcommand' -a doctor -d 'Diagnose configuration and connectivity'
|
|
104
94
|
complete -c aicommit -n '__fish_use_subcommand' -a config -d 'Inspect or validate configuration'
|
|
105
95
|
complete -c aicommit -n '__fish_use_subcommand' -a policy -d 'Print or enforce a repository team policy'
|
|
106
|
-
complete -c aicommit -n '__fish_use_subcommand' -a preset -d 'Manage versioned provider preset manifests'
|
|
107
96
|
complete -c aicommit -n '__fish_use_subcommand' -a completion -d 'Generate shell completion'
|
|
108
97
|
complete -c aicommit -n '__fish_use_subcommand' -a split -d 'Run, plan, apply, resume, or abort split commits'
|
|
109
|
-
complete -c aicommit -n '__fish_use_subcommand' -a stats -d 'Show local quality trends'
|
|
110
98
|
complete -c aicommit -n '__fish_seen_subcommand_from config' -a 'show validate path'
|
|
111
99
|
complete -c aicommit -n '__fish_seen_subcommand_from policy' -a 'template check'
|
|
112
|
-
complete -c aicommit -n '__fish_seen_subcommand_from preset' -a 'show validate path install rollback'
|
|
113
100
|
complete -c aicommit -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish'
|
|
114
101
|
complete -c aicommit -n '__fish_seen_subcommand_from split' -a 'run plan apply resume abort'
|
|
115
|
-
complete -c aicommit -n '__fish_seen_subcommand_from stats' -a 'show clear enable disable'
|
|
116
102
|
complete -c aicommit -s h -l help -d 'Show help'
|
|
117
103
|
complete -c aicommit -s v -l version -d 'Show version'
|
|
118
104
|
complete -c aicommit -s l -l lang -x -a 'zh en' -d 'Commit language'
|
|
119
105
|
complete -c aicommit -s p -l provider -x -d 'Provider name'
|
|
120
106
|
complete -c aicommit -s m -l model -x -d 'Model name'
|
|
121
|
-
complete -c aicommit -l split-hunks -d 'Experimental same-file hunk splitting'
|
|
122
107
|
complete -c aicommit -l scope -x -a 'staged all' -d 'Split plan scope'
|
|
123
108
|
complete -c aicommit -l file -r -d 'Split plan file'
|
|
124
109
|
complete -c aicommit -l range -r -d 'Git range for policy check'
|
package/src/config-command.js
CHANGED
|
@@ -4,25 +4,43 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { getProjectRoot, loadConfig } from './config.js';
|
|
5
5
|
import { fileExists, stringifyConfigRedacted } from './utils.js';
|
|
6
6
|
import { TEAM_POLICY_FILENAME } from './team-policy.js';
|
|
7
|
+
import { projectConfigPath, resolveConfigLocations, userConfigLocations } from './config-paths.js';
|
|
7
8
|
|
|
8
9
|
function redactedObject(value) {
|
|
9
10
|
return JSON.parse(stringifyConfigRedacted(value));
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export async function inspectConfigPaths(projectRoot = getProjectRoot()) {
|
|
13
|
-
const
|
|
14
|
-
const project =
|
|
14
|
+
const userLocations = await resolveConfigLocations(userConfigLocations(homedir()));
|
|
15
|
+
const project = projectConfigPath(projectRoot);
|
|
16
|
+
const projectExists = project !== userLocations.activePath && (await fileExists(project));
|
|
15
17
|
const teamPolicy = join(projectRoot, TEAM_POLICY_FILENAME);
|
|
16
18
|
return {
|
|
17
|
-
user: {
|
|
18
|
-
|
|
19
|
+
user: {
|
|
20
|
+
path: userLocations.canonical,
|
|
21
|
+
exists: Boolean(userLocations.activePath),
|
|
22
|
+
activePath: userLocations.activePath,
|
|
23
|
+
legacyPath: userLocations.legacy,
|
|
24
|
+
legacyExists: userLocations.legacyExists,
|
|
25
|
+
usingLegacy: userLocations.usingLegacy,
|
|
26
|
+
},
|
|
27
|
+
project: {
|
|
28
|
+
path: project,
|
|
29
|
+
exists: projectExists,
|
|
30
|
+
activePath: projectExists ? project : null,
|
|
31
|
+
},
|
|
19
32
|
teamPolicy: { path: teamPolicy, exists: await fileExists(teamPolicy) },
|
|
20
33
|
};
|
|
21
34
|
}
|
|
22
35
|
|
|
23
36
|
function printPaths(paths) {
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
const displayConfigPath = (item) => {
|
|
38
|
+
if (!item.exists) return `${item.path} (not found)`;
|
|
39
|
+
if (item.usingLegacy) return `${item.activePath} (legacy; move to ${item.path})`;
|
|
40
|
+
return item.path;
|
|
41
|
+
};
|
|
42
|
+
console.log(`User config: ${displayConfigPath(paths.user)}`);
|
|
43
|
+
console.log(`Project config: ${displayConfigPath(paths.project)}`);
|
|
26
44
|
console.log(
|
|
27
45
|
`Team policy: ${paths.teamPolicy.path}${paths.teamPolicy.exists ? '' : ' (not found)'}`,
|
|
28
46
|
);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { fileExists } from './utils.js';
|
|
5
|
+
|
|
6
|
+
export const AICOMMIT_DIRECTORY = '.aicommit';
|
|
7
|
+
export const CONFIG_FILENAME = 'config.json';
|
|
8
|
+
export const PROJECT_CONFIG_FILENAME = '.aicommit.config.json';
|
|
9
|
+
|
|
10
|
+
export function userConfigLocations(home = homedir()) {
|
|
11
|
+
return {
|
|
12
|
+
canonical: join(home, AICOMMIT_DIRECTORY, CONFIG_FILENAME),
|
|
13
|
+
legacy: join(home, PROJECT_CONFIG_FILENAME),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function projectConfigPath(projectRoot) {
|
|
18
|
+
return join(projectRoot, PROJECT_CONFIG_FILENAME);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function resolveConfigLocations(locations) {
|
|
22
|
+
const canonicalExists = await fileExists(locations.canonical);
|
|
23
|
+
const legacyExists = await fileExists(locations.legacy);
|
|
24
|
+
return {
|
|
25
|
+
...locations,
|
|
26
|
+
canonicalExists,
|
|
27
|
+
legacyExists,
|
|
28
|
+
activePath: canonicalExists ? locations.canonical : legacyExists ? locations.legacy : null,
|
|
29
|
+
usingLegacy: !canonicalExists && legacyExists,
|
|
30
|
+
};
|
|
31
|
+
}
|