@hifullmoon/aicommit 2.4.1 → 2.6.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 +7 -1
- package/CHANGELOG.md +30 -1
- package/README.md +42 -22
- package/README.zh-CN.md +42 -22
- package/docs/distribution.md +1 -1
- package/docs/large-change-implementation-plan.md +14 -14
- package/docs/privacy.md +2 -2
- package/docs/troubleshooting.md +21 -17
- package/package.json +1 -1
- package/schemas/aicommit-output.schema.json +61 -3
- package/src/analysis-budget.js +17 -7
- package/src/analysis-cache.js +246 -0
- package/src/change-analysis.js +213 -57
- package/src/cli.js +74 -15
- package/src/completion.js +17 -8
- package/src/config.js +30 -2
- package/src/errors.js +6 -2
- package/src/generation-ui.js +1 -1
- package/src/git.js +17 -5
- package/src/local-analysis.js +46 -16
- package/src/main.js +148 -6
- package/src/output.js +19 -2
- package/src/split.js +231 -27
package/src/change-analysis.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
isDeepAnalysis,
|
|
13
13
|
isGeneratedFile,
|
|
14
14
|
localOverview,
|
|
15
|
-
|
|
15
|
+
localPlanBatches,
|
|
16
16
|
} from './local-analysis.js';
|
|
17
17
|
|
|
18
18
|
function gitQuote(path) {
|
|
@@ -230,16 +230,17 @@ const ANALYSIS_SYSTEM =
|
|
|
230
230
|
export function analysisConfig(config) {
|
|
231
231
|
if (config.analysisBudget) return config;
|
|
232
232
|
const settings = { ...DEFAULT_LARGE_CHANGE, ...config.largeChange };
|
|
233
|
+
settings.cache = { ...DEFAULT_LARGE_CHANGE.cache, ...config.largeChange?.cache };
|
|
233
234
|
const model = getProviderAdapter(config).model;
|
|
234
|
-
const output =
|
|
235
|
-
config.
|
|
236
|
-
|
|
237
|
-
|
|
235
|
+
const output =
|
|
236
|
+
config.reasoning?.mode === 'off'
|
|
237
|
+
? config.maxTokens || 1024
|
|
238
|
+
: Math.max(config.maxTokens || 1024, config.reasoning?.maxTokens || 4096);
|
|
238
239
|
settings.chunkInputTokens = Math.min(
|
|
239
240
|
settings.chunkInputTokens,
|
|
240
241
|
Math.max(256, model.contextWindow - output - 1024),
|
|
241
242
|
);
|
|
242
|
-
return { ...config, analysisBudget: createAnalysisBudget(settings) };
|
|
243
|
+
return { ...config, largeChange: settings, analysisBudget: createAnalysisBudget(settings) };
|
|
243
244
|
}
|
|
244
245
|
|
|
245
246
|
export function packItems(items, maxChars, maxItems = 24) {
|
|
@@ -312,7 +313,7 @@ function parseAnalysisJson(raw) {
|
|
|
312
313
|
throw new SyntaxError('Response contains no complete JSON array.');
|
|
313
314
|
}
|
|
314
315
|
|
|
315
|
-
async function jsonCall(config, instruction, items, validate = null) {
|
|
316
|
+
async function jsonCall(config, instruction, items, validate = null, stream = null) {
|
|
316
317
|
const parseAndValidate = (raw) => {
|
|
317
318
|
const parsed = parseAnalysisJson(raw);
|
|
318
319
|
validate?.(parsed);
|
|
@@ -328,7 +329,7 @@ async function jsonCall(config, instruction, items, validate = null) {
|
|
|
328
329
|
Math.min(2048, config.maxTokens || 1024),
|
|
329
330
|
'Return the complete requested JSON array, preserving every required input ID exactly once. ' +
|
|
330
331
|
`Required IDs: ${JSON.stringify(items.map((item) => item.id))}`,
|
|
331
|
-
|
|
332
|
+
stream,
|
|
332
333
|
(response) => {
|
|
333
334
|
try {
|
|
334
335
|
parseAndValidate(response);
|
|
@@ -338,6 +339,7 @@ async function jsonCall(config, instruction, items, validate = null) {
|
|
|
338
339
|
}
|
|
339
340
|
},
|
|
340
341
|
);
|
|
342
|
+
stream?.onReasoningComplete?.(result.reasoning);
|
|
341
343
|
try {
|
|
342
344
|
return parseAndValidate(result.text);
|
|
343
345
|
} catch (cause) {
|
|
@@ -372,63 +374,201 @@ export function validatePartition(groups, ids) {
|
|
|
372
374
|
throw fail(ERROR_CATEGORIES.RESPONSE_FORMAT, 'Analysis left input IDs unassigned.');
|
|
373
375
|
}
|
|
374
376
|
|
|
377
|
+
function fragmentCacheKey(config, input, protect, persistentCache) {
|
|
378
|
+
return persistentCache
|
|
379
|
+
? persistentCache.keyFor('fragment-analysis', input)
|
|
380
|
+
: createHash('sha256')
|
|
381
|
+
.update(
|
|
382
|
+
JSON.stringify({
|
|
383
|
+
version: 1,
|
|
384
|
+
input,
|
|
385
|
+
protect,
|
|
386
|
+
model: config.modelId,
|
|
387
|
+
endpoint: config.apiUrl,
|
|
388
|
+
context: config.repositoryContextText,
|
|
389
|
+
}),
|
|
390
|
+
)
|
|
391
|
+
.digest('hex');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function validateFragmentGroups(candidate, inputIds) {
|
|
395
|
+
validatePartition(candidate, inputIds);
|
|
396
|
+
if (candidate.some((group) => group.ids.length !== 1))
|
|
397
|
+
throw fail(
|
|
398
|
+
ERROR_CATEGORIES.RESPONSE_FORMAT,
|
|
399
|
+
'Fragment analysis combined unrelated source IDs.',
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function estimateInitialDeepCost(
|
|
404
|
+
config,
|
|
405
|
+
capture,
|
|
406
|
+
protect,
|
|
407
|
+
previews,
|
|
408
|
+
maxBatchTokens,
|
|
409
|
+
persistentCache,
|
|
410
|
+
) {
|
|
411
|
+
const batchSizes = [];
|
|
412
|
+
let batch = [];
|
|
413
|
+
let batchTokens = 0;
|
|
414
|
+
let chunks = 0;
|
|
415
|
+
let cachedChunks = 0;
|
|
416
|
+
let oversizedInput = false;
|
|
417
|
+
const fragmentsByFile = new Map();
|
|
418
|
+
const flush = () => {
|
|
419
|
+
if (!batch.length) return;
|
|
420
|
+
chunks++;
|
|
421
|
+
const inputIds = batch.map((item) => item.id);
|
|
422
|
+
const cacheKey = fragmentCacheKey(config, batch, protect, persistentCache);
|
|
423
|
+
const cached =
|
|
424
|
+
capture.analysisCache?.get(cacheKey) ||
|
|
425
|
+
persistentCache?.read(cacheKey, inputIds, (candidate) =>
|
|
426
|
+
validateFragmentGroups(candidate, inputIds),
|
|
427
|
+
);
|
|
428
|
+
if (cached) cachedChunks++;
|
|
429
|
+
else batchSizes.push(batchTokens);
|
|
430
|
+
batch = [];
|
|
431
|
+
batchTokens = 0;
|
|
432
|
+
};
|
|
433
|
+
for (const unit of capture.units(protect, previews)) {
|
|
434
|
+
if (unit.metadataOnly) continue;
|
|
435
|
+
fragmentsByFile.set(unit.fileId, (fragmentsByFile.get(unit.fileId) || 0) + 1);
|
|
436
|
+
const size = estimateTokens(JSON.stringify(unit));
|
|
437
|
+
if (size + 768 > config.analysisBudget.limits.chunkInputTokens) oversizedInput = true;
|
|
438
|
+
if (batch.length && (batchTokens + size > maxBatchTokens || batch.length >= 16)) flush();
|
|
439
|
+
batch.push(unit);
|
|
440
|
+
batchTokens += size;
|
|
441
|
+
}
|
|
442
|
+
flush();
|
|
443
|
+
const output = Math.min(2048, config.maxTokens || 1024);
|
|
444
|
+
let reductionRequests = 0;
|
|
445
|
+
if (config.analysisTask === 'split') {
|
|
446
|
+
for (const fragments of fragmentsByFile.values()) {
|
|
447
|
+
let summaries = fragments;
|
|
448
|
+
while (summaries * 160 > 2000) {
|
|
449
|
+
const batches = Math.ceil(summaries / 24);
|
|
450
|
+
reductionRequests += batches;
|
|
451
|
+
summaries = batches;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
let planningRequests = 1;
|
|
456
|
+
if (config.analysisTask === 'split') {
|
|
457
|
+
planningRequests = 0;
|
|
458
|
+
let candidates = Math.max(1, capture.manifest.length);
|
|
459
|
+
for (let level = 0; level < 8; level++) {
|
|
460
|
+
const batches = Math.ceil(candidates / (config.splitMaxPlanFiles || 100));
|
|
461
|
+
planningRequests += batches;
|
|
462
|
+
if (batches === 1) break;
|
|
463
|
+
candidates = batches;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const downstreamRequests = reductionRequests + planningRequests;
|
|
467
|
+
const downstreamReserve =
|
|
468
|
+
downstreamRequests * (config.analysisBudget.limits.chunkInputTokens + output);
|
|
469
|
+
const estimatedTokens =
|
|
470
|
+
batchSizes.reduce(
|
|
471
|
+
(total, input) =>
|
|
472
|
+
total + Math.min(config.analysisBudget.limits.chunkInputTokens, input + 768) + output,
|
|
473
|
+
0,
|
|
474
|
+
) + downstreamReserve;
|
|
475
|
+
return {
|
|
476
|
+
chunks,
|
|
477
|
+
cachedChunks,
|
|
478
|
+
requestedChunks: batchSizes.length,
|
|
479
|
+
estimatedTokens,
|
|
480
|
+
oversizedInput,
|
|
481
|
+
reductionRequests,
|
|
482
|
+
planningRequests,
|
|
483
|
+
downstreamReserve,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
375
487
|
export async function analyzeChanges(
|
|
376
488
|
config,
|
|
377
489
|
capture,
|
|
378
490
|
protect = true,
|
|
379
491
|
previews = null,
|
|
380
492
|
onProgress = null,
|
|
493
|
+
persistentCache = null,
|
|
381
494
|
) {
|
|
382
495
|
if (!isDeepAnalysis(config)) return analyzeLocally(config, capture, protect, previews);
|
|
383
496
|
const budget = config.analysisBudget;
|
|
497
|
+
const maxBatchTokens = Math.min(
|
|
498
|
+
estimateTokens('x'.repeat(config.maxDiffChars || 30000)),
|
|
499
|
+
Math.floor(budget.limits.chunkInputTokens * 0.65),
|
|
500
|
+
);
|
|
501
|
+
// chunkInputTokens is a token budget, while String#length counts UTF-16 code
|
|
502
|
+
// units. Track the same conservative UTF-8 estimate used at dispatch time so
|
|
503
|
+
// ASCII-heavy repositories do not produce roughly twice as many requests and
|
|
504
|
+
// multibyte input still stays within the configured ceiling.
|
|
384
505
|
const maxChars = Math.min(
|
|
385
506
|
config.maxDiffChars || 30000,
|
|
386
507
|
Math.floor(budget.limits.chunkInputTokens * 0.65),
|
|
387
508
|
);
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
509
|
+
const estimate = estimateInitialDeepCost(
|
|
510
|
+
config,
|
|
511
|
+
capture,
|
|
512
|
+
protect,
|
|
513
|
+
previews,
|
|
514
|
+
maxBatchTokens,
|
|
515
|
+
persistentCache,
|
|
391
516
|
);
|
|
517
|
+
if (estimate.oversizedInput || estimate.estimatedTokens > budget.limits.maxTotalTokens) {
|
|
518
|
+
const fallbackConfig = {
|
|
519
|
+
...config,
|
|
520
|
+
largeChange: { ...config.largeChange, strategy: 'auto' },
|
|
521
|
+
};
|
|
522
|
+
const local = analyzeLocally(fallbackConfig, capture, protect, previews);
|
|
523
|
+
local.coverage = {
|
|
524
|
+
...local.coverage,
|
|
525
|
+
degradedFrom: 'deep',
|
|
526
|
+
fallbackReason: estimate.oversizedInput ? 'preflight_input' : 'preflight_tokens',
|
|
527
|
+
estimatedDeepChunks: estimate.chunks,
|
|
528
|
+
estimatedCachedChunks: estimate.cachedChunks,
|
|
529
|
+
estimatedRequestedChunks: estimate.requestedChunks,
|
|
530
|
+
estimatedReductionRequests: estimate.reductionRequests,
|
|
531
|
+
estimatedPlanningRequests: estimate.planningRequests,
|
|
532
|
+
estimatedDeepTokens: estimate.estimatedTokens,
|
|
533
|
+
};
|
|
534
|
+
return local;
|
|
535
|
+
}
|
|
536
|
+
budget.reserveFinal(estimate.downstreamReserve);
|
|
392
537
|
const fileFacts = new Map(capture.manifest.map((f) => [f.id, []]));
|
|
393
538
|
let batch = [];
|
|
394
|
-
let
|
|
539
|
+
let batchTokens = 0;
|
|
395
540
|
let done = 0;
|
|
396
541
|
let analyzed = 0;
|
|
542
|
+
let cachedChunks = 0;
|
|
543
|
+
let requestedChunks = 0;
|
|
397
544
|
const pending = [];
|
|
398
545
|
const cache = (capture.analysisCache ||= new Map());
|
|
399
546
|
async function flush() {
|
|
400
547
|
if (!batch.length) return;
|
|
401
548
|
const input = batch;
|
|
402
549
|
batch = [];
|
|
403
|
-
|
|
404
|
-
const cacheKey =
|
|
405
|
-
.update(
|
|
406
|
-
JSON.stringify({
|
|
407
|
-
version: 1,
|
|
408
|
-
input,
|
|
409
|
-
protect,
|
|
410
|
-
model: config.modelId,
|
|
411
|
-
endpoint: config.apiUrl,
|
|
412
|
-
context: config.repositoryContextText,
|
|
413
|
-
}),
|
|
414
|
-
)
|
|
415
|
-
.digest('hex');
|
|
550
|
+
batchTokens = 0;
|
|
551
|
+
const cacheKey = fragmentCacheKey(config, input, protect, persistentCache);
|
|
416
552
|
const inputIds = input.map((x) => x.id);
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
553
|
+
const validateGroups = (candidate) => validateFragmentGroups(candidate, inputIds);
|
|
554
|
+
let groups = cache.get(cacheKey);
|
|
555
|
+
let cacheHit = Boolean(groups);
|
|
556
|
+
if (!groups && persistentCache) {
|
|
557
|
+
groups = persistentCache.read(cacheKey, inputIds, validateGroups);
|
|
558
|
+
cacheHit = Boolean(groups);
|
|
559
|
+
}
|
|
560
|
+
if (!groups) {
|
|
561
|
+
groups = await jsonCall(
|
|
420
562
|
config,
|
|
421
563
|
'Return one entry per input ID: [{"ids":["input ID"],"summary":"concise factual changes and uncertainties"}]. Each input ID must appear exactly once. Never combine different IDs. Summaries must be at most 160 characters.',
|
|
422
564
|
input,
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
},
|
|
431
|
-
));
|
|
565
|
+
validateGroups,
|
|
566
|
+
);
|
|
567
|
+
requestedChunks++;
|
|
568
|
+
persistentCache?.write(cacheKey, inputIds, groups);
|
|
569
|
+
} else {
|
|
570
|
+
cachedChunks++;
|
|
571
|
+
}
|
|
432
572
|
const byId = new Map(input.map((x) => [x.id, x.fileId]));
|
|
433
573
|
cache.set(cacheKey, groups);
|
|
434
574
|
for (const group of groups) {
|
|
@@ -436,7 +576,13 @@ export async function analyzeChanges(
|
|
|
436
576
|
fileFacts.get(id).push(group.summary);
|
|
437
577
|
}
|
|
438
578
|
analyzed += input.filter((x) => !x.metadataOnly).length;
|
|
439
|
-
onProgress?.({
|
|
579
|
+
onProgress?.({
|
|
580
|
+
completedChunks: ++done,
|
|
581
|
+
analyzedFragments: analyzed,
|
|
582
|
+
cachedChunks,
|
|
583
|
+
requestedChunks,
|
|
584
|
+
cacheHit,
|
|
585
|
+
});
|
|
440
586
|
}
|
|
441
587
|
async function schedule() {
|
|
442
588
|
pending.push(
|
|
@@ -456,10 +602,11 @@ export async function analyzeChanges(
|
|
|
456
602
|
fileFacts.get(unit.fileId).push(unit.text);
|
|
457
603
|
continue;
|
|
458
604
|
}
|
|
459
|
-
const size = JSON.stringify(unit)
|
|
460
|
-
if (batch.length && (
|
|
605
|
+
const size = estimateTokens(JSON.stringify(unit));
|
|
606
|
+
if (batch.length && (batchTokens + size > maxBatchTokens || batch.length >= 16))
|
|
607
|
+
await schedule();
|
|
461
608
|
batch.push(unit);
|
|
462
|
-
|
|
609
|
+
batchTokens += size;
|
|
463
610
|
}
|
|
464
611
|
await schedule();
|
|
465
612
|
for (const err of await Promise.all(pending)) if (err) throw err;
|
|
@@ -510,15 +657,22 @@ export async function analyzeChanges(
|
|
|
510
657
|
metadataOnlyFiles: capture.manifest.filter((f) => f.metadataOnly).length,
|
|
511
658
|
failedFiles: 0,
|
|
512
659
|
completedChunks: done,
|
|
660
|
+
cachedChunks,
|
|
661
|
+
requestedChunks,
|
|
513
662
|
},
|
|
514
663
|
};
|
|
515
664
|
} catch (err) {
|
|
665
|
+
const exhausted = err.data?.analysis?.exhausted || (!budget.remainingMs() ? 'time' : undefined);
|
|
516
666
|
err.data = {
|
|
517
667
|
...err.data,
|
|
518
668
|
analysis: {
|
|
519
669
|
...budget.snapshot(),
|
|
670
|
+
...err.data?.analysis,
|
|
671
|
+
...(exhausted ? { exhausted } : {}),
|
|
520
672
|
totalFiles: capture.manifest.length,
|
|
521
673
|
completedChunks: done,
|
|
674
|
+
cachedChunks,
|
|
675
|
+
requestedChunks,
|
|
522
676
|
complete: false,
|
|
523
677
|
},
|
|
524
678
|
};
|
|
@@ -559,8 +713,9 @@ export async function summarizeChanges(config, facts) {
|
|
|
559
713
|
return `Structured change summaries (not raw diff):\n${JSON.stringify(items)}`;
|
|
560
714
|
}
|
|
561
715
|
|
|
562
|
-
export async function planAnalyzedChanges(config, facts, coverage = null) {
|
|
716
|
+
export async function planAnalyzedChanges(config, facts, coverage = null, stream = null) {
|
|
563
717
|
let candidates = facts;
|
|
718
|
+
const deep = isDeepAnalysis(config) && coverage?.strategy !== 'auto';
|
|
564
719
|
const policy = normalizeCommitPolicy(config.commitPolicy, config.language);
|
|
565
720
|
const cap = Math.min(
|
|
566
721
|
config.splitMaxDiffChars || 16000,
|
|
@@ -568,15 +723,16 @@ export async function planAnalyzedChanges(config, facts, coverage = null) {
|
|
|
568
723
|
);
|
|
569
724
|
for (let level = 0; level < 8; level++) {
|
|
570
725
|
const byId = new Map(candidates.map((x) => [x.id, x]));
|
|
571
|
-
const batches =
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
726
|
+
const batches =
|
|
727
|
+
!deep && level === 0
|
|
728
|
+
? localPlanBatches(config, candidates, cap)
|
|
729
|
+
: packItems(
|
|
730
|
+
candidates.map(({ id, path, summary }) => ({ id, path, summary })),
|
|
731
|
+
cap,
|
|
732
|
+
config.splitMaxPlanFiles || 100,
|
|
733
|
+
);
|
|
734
|
+
if (!deep && level === 0 && coverage) {
|
|
735
|
+
coverage.sampledFiles = batches.flat().filter((item) => item.representativeExcerpt).length;
|
|
580
736
|
coverage.metadataOnlyFiles = coverage.totalFiles - coverage.sampledFiles;
|
|
581
737
|
}
|
|
582
738
|
const next = [];
|
|
@@ -594,6 +750,9 @@ export async function planAnalyzedChanges(config, facts, coverage = null) {
|
|
|
594
750
|
'Analysis plan is missing a commit subject.',
|
|
595
751
|
);
|
|
596
752
|
},
|
|
753
|
+
// Earlier batches may run many requests. Surface thinking from the
|
|
754
|
+
// final plan, which is the reasoning relevant to the review screen.
|
|
755
|
+
batches.length === 1 ? stream : null,
|
|
597
756
|
);
|
|
598
757
|
for (const group of groups) {
|
|
599
758
|
next.push({
|
|
@@ -605,14 +764,11 @@ export async function planAnalyzedChanges(config, facts, coverage = null) {
|
|
|
605
764
|
}
|
|
606
765
|
if (batches.length === 1)
|
|
607
766
|
return next.map(({ subject, body, files }) => ({ subject, body, files }));
|
|
608
|
-
if (next.length >= candidates.length)
|
|
609
|
-
throw fail(
|
|
610
|
-
ERROR_CATEGORIES.PROVIDER,
|
|
611
|
-
'Too many independent change groups for a complete global plan. Stage a smaller logical change or increase planning limits.',
|
|
612
|
-
);
|
|
613
767
|
candidates = next;
|
|
614
768
|
}
|
|
615
|
-
throw fail(ERROR_CATEGORIES.PROVIDER, 'Split planning exceeded the maximum summary depth.'
|
|
769
|
+
throw fail(ERROR_CATEGORIES.PROVIDER, 'Split planning exceeded the maximum summary depth.', {
|
|
770
|
+
data: { fallbackPlan: true },
|
|
771
|
+
});
|
|
616
772
|
}
|
|
617
773
|
|
|
618
774
|
export function needsAnalysis(capture, config, split = false) {
|
package/src/cli.js
CHANGED
|
@@ -16,13 +16,17 @@ function showHelp() {
|
|
|
16
16
|
${chalk.dim('$')} aicommit [path] [options]
|
|
17
17
|
${chalk.dim('$')} aicommit setup
|
|
18
18
|
${chalk.dim('$')} aicommit update
|
|
19
|
-
${chalk.dim('$')} aicommit
|
|
19
|
+
${chalk.dim('$')} aicommit generate --scope=staged|all --file=<path> [options]
|
|
20
|
+
${chalk.dim('$')} aicommit apply --file=<path> [options]
|
|
21
|
+
${chalk.dim('$')} aicommit split [run|plan|apply|resume|abort|status] [options]
|
|
20
22
|
|
|
21
23
|
${chalk.bold('Everyday commands:')}
|
|
22
24
|
setup Interactive configuration wizard
|
|
23
25
|
update Update the global npm installation to latest
|
|
24
26
|
doctor Diagnose runtime, config, credentials, and connectivity
|
|
25
27
|
split Plan and create file-level logical commits
|
|
28
|
+
generate Export one validated commit plan without committing
|
|
29
|
+
apply Apply a validated commit plan
|
|
26
30
|
|
|
27
31
|
${chalk.bold('Advanced commands:')}
|
|
28
32
|
config show Print the effective configuration with secrets redacted
|
|
@@ -35,6 +39,7 @@ function showHelp() {
|
|
|
35
39
|
split apply Validate and apply an exported JSON plan
|
|
36
40
|
split resume Resume the repository's unfinished transaction
|
|
37
41
|
split abort Discard recovery metadata; keep commits and changes
|
|
42
|
+
split status Inspect recovery metadata without changing Git state
|
|
38
43
|
|
|
39
44
|
${chalk.bold('Arguments:')}
|
|
40
45
|
path Target directory (default: current directory)
|
|
@@ -45,13 +50,14 @@ function showHelp() {
|
|
|
45
50
|
-l, --lang=<zh|en> Commit message language (default: zh)
|
|
46
51
|
-p, --provider=<name> Use the named provider from config "providers"
|
|
47
52
|
-m, --model=<name> Use a named model from the selected provider
|
|
48
|
-
--scope=<scope> Scope for
|
|
49
|
-
--file=<path>
|
|
53
|
+
--scope=<scope> Scope for dry-run, generate, or split planning: staged, all
|
|
54
|
+
--file=<path> Commit-plan artifact or commit-message file
|
|
50
55
|
--range=<revision> Git revision/range for "policy check" (default: HEAD)
|
|
51
56
|
--reasoning=<level> Set reasoning effort (enabled by default: medium)
|
|
52
57
|
--no-reasoning Explicitly disable reasoning when supported
|
|
53
58
|
--dry-run Generate and review without creating commits
|
|
54
59
|
-y, --yes Non-interactive: accept the generated message/plan
|
|
60
|
+
--allow-single-fallback Let non-interactive split commit one complete group if planning exhausts its budget
|
|
55
61
|
--output=<text|json> Output mode; JSON requires --yes for commit flows
|
|
56
62
|
--debug Print debug info (parsed args, final config, etc.)
|
|
57
63
|
|
|
@@ -90,9 +96,13 @@ function parsedDefaults(overrides = {}) {
|
|
|
90
96
|
debug: false,
|
|
91
97
|
split: null,
|
|
92
98
|
splitCommand: null,
|
|
99
|
+
generate: false,
|
|
100
|
+
generateScope: null,
|
|
101
|
+
previewScope: null,
|
|
93
102
|
splitPlanFile: null,
|
|
94
103
|
dryRun: false,
|
|
95
104
|
yes: false,
|
|
105
|
+
allowSingleFallback: false,
|
|
96
106
|
setup: false,
|
|
97
107
|
update: false,
|
|
98
108
|
doctor: false,
|
|
@@ -163,10 +173,17 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
163
173
|
args = args.slice(2);
|
|
164
174
|
}
|
|
165
175
|
|
|
176
|
+
const generate = !update && args[0] === 'generate';
|
|
177
|
+
if (generate) args = args.slice(1);
|
|
178
|
+
|
|
166
179
|
let splitCommand = null;
|
|
180
|
+
if (!update && args[0] === 'apply') {
|
|
181
|
+
splitCommand = 'apply';
|
|
182
|
+
args = args.slice(1);
|
|
183
|
+
}
|
|
167
184
|
if (!update && args[0] === 'split') {
|
|
168
185
|
const requestedAction = args[1];
|
|
169
|
-
const actions = ['run', 'plan', 'apply', 'resume', 'abort'];
|
|
186
|
+
const actions = ['run', 'plan', 'apply', 'resume', 'abort', 'status'];
|
|
170
187
|
if (!requestedAction || requestedAction.startsWith('-')) {
|
|
171
188
|
// Keep the common path short: `aicommit split` is the interactive
|
|
172
189
|
// split flow, while explicit actions remain available for automation
|
|
@@ -179,7 +196,7 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
179
196
|
} else {
|
|
180
197
|
throw fail(
|
|
181
198
|
ERROR_CATEGORIES.CONFIG,
|
|
182
|
-
'split requires one action: run, plan, apply, resume, or
|
|
199
|
+
'split requires one action: run, plan, apply, resume, abort, or status.',
|
|
183
200
|
);
|
|
184
201
|
}
|
|
185
202
|
}
|
|
@@ -195,12 +212,15 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
195
212
|
let output = 'text';
|
|
196
213
|
let debug = false;
|
|
197
214
|
let split = null;
|
|
215
|
+
let generateScope = null;
|
|
216
|
+
let previewScope = null;
|
|
198
217
|
let splitPlanFile = null;
|
|
199
218
|
let policyMessageFile = null;
|
|
200
219
|
let policyRange = null;
|
|
201
220
|
let splitScopeOption = false;
|
|
202
221
|
let dryRun = false;
|
|
203
222
|
let yes = false;
|
|
223
|
+
let allowSingleFallback = false;
|
|
204
224
|
const setup = false;
|
|
205
225
|
|
|
206
226
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -235,20 +255,26 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
235
255
|
|
|
236
256
|
if (arg === '--scope') {
|
|
237
257
|
splitScopeOption = true;
|
|
238
|
-
|
|
258
|
+
const scope = takeValue(args, i, arg, 'staged|all');
|
|
239
259
|
i++;
|
|
240
|
-
if (!['staged', 'all'].includes(
|
|
241
|
-
throw fail(ERROR_CATEGORIES.CONFIG, `Invalid split scope: "${
|
|
260
|
+
if (!['staged', 'all'].includes(scope)) {
|
|
261
|
+
throw fail(ERROR_CATEGORIES.CONFIG, `Invalid split scope: "${scope}". Use staged or all.`);
|
|
242
262
|
}
|
|
263
|
+
if (generate) generateScope = scope;
|
|
264
|
+
else if (splitCommand) split = scope;
|
|
265
|
+
else previewScope = scope;
|
|
243
266
|
continue;
|
|
244
267
|
}
|
|
245
268
|
|
|
246
269
|
if (arg.startsWith('--scope=')) {
|
|
247
270
|
splitScopeOption = true;
|
|
248
|
-
|
|
249
|
-
if (!['staged', 'all'].includes(
|
|
250
|
-
throw fail(ERROR_CATEGORIES.CONFIG, `Invalid split scope: "${
|
|
271
|
+
const scope = arg.slice('--scope='.length);
|
|
272
|
+
if (!['staged', 'all'].includes(scope)) {
|
|
273
|
+
throw fail(ERROR_CATEGORIES.CONFIG, `Invalid split scope: "${scope}". Use staged or all.`);
|
|
251
274
|
}
|
|
275
|
+
if (generate) generateScope = scope;
|
|
276
|
+
else if (splitCommand) split = scope;
|
|
277
|
+
else previewScope = scope;
|
|
252
278
|
continue;
|
|
253
279
|
}
|
|
254
280
|
|
|
@@ -290,6 +316,11 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
290
316
|
continue;
|
|
291
317
|
}
|
|
292
318
|
|
|
319
|
+
if (arg === '--allow-single-fallback') {
|
|
320
|
+
allowSingleFallback = true;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
293
324
|
if (arg === '--reasoning') {
|
|
294
325
|
cliReasoning = takeValue(args, i, arg, 'low|medium|high|xhigh|max');
|
|
295
326
|
i++;
|
|
@@ -419,14 +450,38 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
419
450
|
}
|
|
420
451
|
}
|
|
421
452
|
}
|
|
422
|
-
if (
|
|
453
|
+
if (generate) {
|
|
454
|
+
if (!generateScope || !splitPlanFile) {
|
|
455
|
+
throw fail(
|
|
456
|
+
ERROR_CATEGORIES.CONFIG,
|
|
457
|
+
'generate requires --scope=staged|all and --file=<path>.',
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
if (splitCommand || allowSingleFallback) {
|
|
461
|
+
throw fail(ERROR_CATEGORIES.CONFIG, 'generate does not accept split commit options.');
|
|
462
|
+
}
|
|
463
|
+
dryRun = true;
|
|
464
|
+
}
|
|
465
|
+
if (splitScopeOption && !generate && splitCommand && !['run', 'plan'].includes(splitCommand)) {
|
|
423
466
|
throw fail(
|
|
424
467
|
ERROR_CATEGORIES.CONFIG,
|
|
425
|
-
'--scope is only valid with
|
|
468
|
+
'--scope is only valid with dry-run, generate, split run, or split plan.',
|
|
426
469
|
);
|
|
427
470
|
}
|
|
428
|
-
if (
|
|
429
|
-
throw fail(ERROR_CATEGORIES.CONFIG, '--
|
|
471
|
+
if (previewScope && !dryRun) {
|
|
472
|
+
throw fail(ERROR_CATEGORIES.CONFIG, '--scope without a subcommand requires --dry-run.');
|
|
473
|
+
}
|
|
474
|
+
if (splitPlanFile && !generate && !['plan', 'apply'].includes(splitCommand)) {
|
|
475
|
+
throw fail(
|
|
476
|
+
ERROR_CATEGORIES.CONFIG,
|
|
477
|
+
'--file is only valid with generate, apply, or split plan/apply.',
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
if (allowSingleFallback && (splitCommand !== 'run' || !yes || dryRun)) {
|
|
481
|
+
throw fail(
|
|
482
|
+
ERROR_CATEGORIES.CONFIG,
|
|
483
|
+
'--allow-single-fallback requires non-dry-run "aicommit split run --yes".',
|
|
484
|
+
);
|
|
430
485
|
}
|
|
431
486
|
if (
|
|
432
487
|
configAction &&
|
|
@@ -494,9 +549,13 @@ export function parseArgs(args = process.argv.slice(2)) {
|
|
|
494
549
|
debug,
|
|
495
550
|
split,
|
|
496
551
|
splitCommand,
|
|
552
|
+
generate,
|
|
553
|
+
generateScope,
|
|
554
|
+
previewScope,
|
|
497
555
|
splitPlanFile,
|
|
498
556
|
dryRun,
|
|
499
557
|
yes,
|
|
558
|
+
allowSingleFallback,
|
|
500
559
|
setup,
|
|
501
560
|
update,
|
|
502
561
|
doctor,
|