@klarkxy/dsh-git-commit 0.1.6 → 0.2.1
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/README.md +34 -4
- package/docs/README.zh-CN.md +28 -2
- package/lib/client.inner.cjs +395 -79
- package/lib/client.inner.cjs.map +1 -1
- package/lib/client.js +396 -241
- package/lib/contracts.d.ts +45 -12
- package/lib/contracts.js +1 -27
- package/lib/contracts.js.map +1 -1
- package/lib/index.d.ts +14 -5
- package/lib/index.js +294 -82
- package/lib/index.js.map +1 -1
- package/package.json +16 -9
package/lib/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { DEFAULT_MAX_OUTPUT_TOKENS, MAX_MANIFEST_CHARS, MAX_UNTRACKED_PREVIEW_CHARS, PLUGIN_NAME, RPC_CHANNEL, SETTINGS_KEY, defaultSettings, normalizeFallback, normalizeModelRoute } from "./contracts.js";
|
|
2
2
|
import { registerHostRpc } from "@klarkxy/dsh-plugin-kit/host-rpc";
|
|
3
|
+
import { notifyModelSettingsChanged, registerModelSettings } from "@klarkxy/dsh-plugin-kit/model-settings";
|
|
4
|
+
import { modelRouteOverride } from "@klarkxy/dsh-model-route";
|
|
3
5
|
import { callLlmText, resolveFeatureModel } from "@klarkxy/dsh-plugin-kit";
|
|
4
|
-
import { modelMenuOverride } from "@klarkxy/dsh-plugin-kit/model-menu";
|
|
5
6
|
import { execFile } from "node:child_process";
|
|
6
7
|
import { createHash } from "node:crypto";
|
|
7
8
|
import { constants } from "node:fs";
|
|
@@ -343,6 +344,19 @@ async function untrackedPreview(root, path) {
|
|
|
343
344
|
};
|
|
344
345
|
}
|
|
345
346
|
}
|
|
347
|
+
/** Known commits or an uncertain current group must survive a rejected run. */
|
|
348
|
+
var PartialCommitError = class extends Error {
|
|
349
|
+
commits;
|
|
350
|
+
cause;
|
|
351
|
+
uncertain;
|
|
352
|
+
constructor(commits, cause, uncertain) {
|
|
353
|
+
super(cause instanceof Error ? cause.message : String(cause));
|
|
354
|
+
this.name = "PartialCommitError";
|
|
355
|
+
this.commits = commits;
|
|
356
|
+
this.cause = cause;
|
|
357
|
+
this.uncertain = uncertain;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
346
360
|
async function indexBytes(path) {
|
|
347
361
|
try {
|
|
348
362
|
return await readFile(path);
|
|
@@ -369,26 +383,38 @@ async function headOid(run, root) {
|
|
|
369
383
|
*/
|
|
370
384
|
async function commitGroups(run, root, groups, selected, signal, verify) {
|
|
371
385
|
const commits = [];
|
|
386
|
+
let partialFailure;
|
|
387
|
+
const fail = (error) => {
|
|
388
|
+
if (error instanceof PartialCommitError) {
|
|
389
|
+
partialFailure = error;
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
if (commits.length === 0) throw error;
|
|
393
|
+
partialFailure = new PartialCommitError(commits, error);
|
|
394
|
+
throw partialFailure;
|
|
395
|
+
};
|
|
372
396
|
for (const group of groups) {
|
|
373
397
|
if (group.files.length === 0) continue;
|
|
374
398
|
const paths = group.files.flatMap((path) => {
|
|
375
399
|
const file = selected.find((file) => file.path === path);
|
|
376
400
|
return file?.status === "renamed" && file.origin ? [path, file.origin] : [path];
|
|
377
401
|
});
|
|
378
|
-
|
|
379
|
-
const gitPath = (await run([
|
|
380
|
-
"rev-parse",
|
|
381
|
-
"--git-path",
|
|
382
|
-
"index"
|
|
383
|
-
], {
|
|
384
|
-
cwd: root,
|
|
385
|
-
signal
|
|
386
|
-
})).trim();
|
|
387
|
-
const indexPath = isAbsolute(gitPath) ? gitPath : resolve(root, gitPath);
|
|
388
|
-
const backupDir = await mkdtemp(join(tmpdir(), "dsh-git-commit-"));
|
|
389
|
-
const backupPath = join(backupDir, "index");
|
|
390
|
-
let hadIndex = true;
|
|
402
|
+
let backupDir;
|
|
391
403
|
try {
|
|
404
|
+
signal?.throwIfAborted();
|
|
405
|
+
await verify?.(paths);
|
|
406
|
+
const gitPath = (await run([
|
|
407
|
+
"rev-parse",
|
|
408
|
+
"--git-path",
|
|
409
|
+
"index"
|
|
410
|
+
], {
|
|
411
|
+
cwd: root,
|
|
412
|
+
signal
|
|
413
|
+
})).trim();
|
|
414
|
+
const indexPath = isAbsolute(gitPath) ? gitPath : resolve(root, gitPath);
|
|
415
|
+
backupDir = await mkdtemp(join(tmpdir(), "dsh-git-commit-"));
|
|
416
|
+
const backupPath = join(backupDir, "index");
|
|
417
|
+
let hadIndex = true;
|
|
392
418
|
try {
|
|
393
419
|
await copyFile(indexPath, backupPath);
|
|
394
420
|
} catch (error) {
|
|
@@ -399,7 +425,10 @@ async function commitGroups(run, root, groups, selected, signal, verify) {
|
|
|
399
425
|
const originalIndex = await indexBytes(indexPath);
|
|
400
426
|
if (hadIndex ? !originalIndex?.equals(await readFile(backupPath)) : originalIndex !== void 0) throw new GitError("dsh-git-commit: index changed before committing; retry");
|
|
401
427
|
let stagedIndex = originalIndex;
|
|
428
|
+
let committed = false;
|
|
429
|
+
let commitInvoked = false;
|
|
402
430
|
try {
|
|
431
|
+
signal?.throwIfAborted();
|
|
403
432
|
await run([
|
|
404
433
|
"add",
|
|
405
434
|
"-A",
|
|
@@ -410,6 +439,8 @@ async function commitGroups(run, root, groups, selected, signal, verify) {
|
|
|
410
439
|
signal
|
|
411
440
|
});
|
|
412
441
|
stagedIndex = await indexBytes(indexPath);
|
|
442
|
+
signal?.throwIfAborted();
|
|
443
|
+
commitInvoked = true;
|
|
413
444
|
await run([
|
|
414
445
|
"commit",
|
|
415
446
|
"--quiet",
|
|
@@ -422,33 +453,71 @@ async function commitGroups(run, root, groups, selected, signal, verify) {
|
|
|
422
453
|
cwd: root,
|
|
423
454
|
signal
|
|
424
455
|
});
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
456
|
+
committed = true;
|
|
457
|
+
let hash = "";
|
|
458
|
+
try {
|
|
459
|
+
hash = (await run([
|
|
460
|
+
"rev-parse",
|
|
461
|
+
"--short",
|
|
462
|
+
"HEAD"
|
|
463
|
+
], { cwd: root })).trim();
|
|
464
|
+
} catch (error) {
|
|
465
|
+
commits.push({
|
|
466
|
+
hash: "",
|
|
467
|
+
skipped: false
|
|
468
|
+
});
|
|
469
|
+
throw error;
|
|
470
|
+
}
|
|
471
|
+
if (!hash) {
|
|
472
|
+
commits.push({
|
|
473
|
+
hash: "",
|
|
474
|
+
skipped: false
|
|
475
|
+
});
|
|
476
|
+
throw new GitError("dsh-git-commit: commit was created but its hash could not be read");
|
|
477
|
+
}
|
|
433
478
|
commits.push({
|
|
434
479
|
hash,
|
|
435
480
|
skipped: false
|
|
436
481
|
});
|
|
437
482
|
} catch (error) {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
483
|
+
if (!committed) {
|
|
484
|
+
let currentHead;
|
|
485
|
+
try {
|
|
486
|
+
currentHead = await headOid(run, root);
|
|
487
|
+
} catch {
|
|
488
|
+
if (commitInvoked) throw new PartialCommitError(commits, error, {
|
|
489
|
+
message: group.message,
|
|
490
|
+
files: [...group.files]
|
|
491
|
+
});
|
|
492
|
+
throw error;
|
|
493
|
+
}
|
|
494
|
+
if (currentHead !== head) {
|
|
495
|
+
if (commitInvoked) throw new PartialCommitError(commits, error, {
|
|
496
|
+
message: group.message,
|
|
497
|
+
files: [...group.files]
|
|
498
|
+
});
|
|
499
|
+
throw error;
|
|
500
|
+
}
|
|
501
|
+
const currentIndex = await indexBytes(indexPath);
|
|
502
|
+
const indexUntouched = currentIndex === void 0 ? stagedIndex === void 0 : currentIndex.equals(stagedIndex ?? Buffer.alloc(0));
|
|
503
|
+
if (currentHead === head && indexUntouched) {
|
|
504
|
+
if (hadIndex) await copyFile(backupPath, indexPath);
|
|
505
|
+
else await rm(indexPath, { force: true });
|
|
506
|
+
}
|
|
444
507
|
}
|
|
445
508
|
throw error;
|
|
446
509
|
}
|
|
510
|
+
} catch (error) {
|
|
511
|
+
fail(error);
|
|
447
512
|
} finally {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
513
|
+
if (backupDir) try {
|
|
514
|
+
await rm(backupDir, {
|
|
515
|
+
recursive: true,
|
|
516
|
+
force: true
|
|
517
|
+
});
|
|
518
|
+
} catch (error) {
|
|
519
|
+
fail(partialFailure ?? error);
|
|
520
|
+
}
|
|
452
521
|
}
|
|
453
522
|
}
|
|
454
523
|
return commits;
|
|
@@ -563,10 +632,13 @@ function parsePlan(text, files) {
|
|
|
563
632
|
//#region src/service.ts
|
|
564
633
|
var CommitRunError = class extends Error {
|
|
565
634
|
code;
|
|
566
|
-
|
|
635
|
+
/** Commits already created when this run stopped. Absent when nothing was written. */
|
|
636
|
+
partial;
|
|
637
|
+
constructor(code, message, partial) {
|
|
567
638
|
super(message);
|
|
568
|
-
this.name = "CommitRunError";
|
|
639
|
+
this.name = code === "cancelled" ? "AbortError" : "CommitRunError";
|
|
569
640
|
this.code = code;
|
|
641
|
+
this.partial = partial;
|
|
570
642
|
}
|
|
571
643
|
};
|
|
572
644
|
function modelInfo(route, source) {
|
|
@@ -594,36 +666,83 @@ var GitCommitService = class {
|
|
|
594
666
|
settings;
|
|
595
667
|
settingsPending = Promise.resolve();
|
|
596
668
|
settingsReady;
|
|
669
|
+
settingsFailure;
|
|
670
|
+
settingsAdopted = false;
|
|
597
671
|
disposed = false;
|
|
672
|
+
/** Aborted on unload so an in-flight model plan receives the same signal as a cancelled request. */
|
|
673
|
+
lifetime = new AbortController();
|
|
598
674
|
constructor(deps) {
|
|
599
675
|
this.deps = deps;
|
|
600
676
|
this.run = deps.run ?? execGit;
|
|
601
677
|
this.preview = deps.preview ?? untrackedPreview;
|
|
602
678
|
this.settings = defaultSettings();
|
|
603
679
|
this.settingsReady = deps.settings?.load().then((loaded) => {
|
|
604
|
-
if (loaded) this.settings = structuredClone(loaded);
|
|
680
|
+
if (loaded && !this.settingsAdopted) this.settings = structuredClone(loaded);
|
|
605
681
|
}, () => {
|
|
606
|
-
this.
|
|
682
|
+
if (!this.settingsAdopted) this.settingsFailure = new CommitRunError("settings-unavailable", "dsh-git-commit: saved settings could not be read; reload the plugin before retrying");
|
|
607
683
|
}) ?? Promise.resolve();
|
|
608
684
|
}
|
|
609
|
-
/**
|
|
685
|
+
/** A refreshed row wins over a pending initial read. */
|
|
610
686
|
adoptSettings(next) {
|
|
687
|
+
if (this.disposed) throw new CommitRunError("disposed", "dsh-git-commit: plugin disabled");
|
|
688
|
+
this.settingsAdopted = true;
|
|
689
|
+
this.settingsFailure = void 0;
|
|
611
690
|
this.settings = structuredClone(next);
|
|
612
691
|
}
|
|
692
|
+
/** Existing synchronous snapshot API; host operations wait for initialization first. */
|
|
613
693
|
getSettings() {
|
|
694
|
+
this.workSignal();
|
|
695
|
+
if (this.settingsFailure) throw this.settingsFailure;
|
|
614
696
|
return structuredClone(this.settings);
|
|
615
697
|
}
|
|
616
|
-
|
|
617
|
-
|
|
698
|
+
async ready() {
|
|
699
|
+
this.workSignal();
|
|
700
|
+
await this.settingsReady;
|
|
701
|
+
this.workSignal();
|
|
702
|
+
if (this.settingsFailure) throw this.settingsFailure;
|
|
703
|
+
}
|
|
704
|
+
/** Request cancellation and plugin unload share one signal. A disposed service rejects before any new work. */
|
|
705
|
+
workSignal(signal) {
|
|
706
|
+
if (this.disposed || this.lifetime.signal.aborted) throw new CommitRunError("disposed", "dsh-git-commit: plugin is no longer active");
|
|
707
|
+
return signal ? AbortSignal.any([signal, this.lifetime.signal]) : this.lifetime.signal;
|
|
708
|
+
}
|
|
709
|
+
async waitForSettings(signal) {
|
|
710
|
+
signal?.throwIfAborted();
|
|
711
|
+
if (!signal) {
|
|
712
|
+
await this.ready();
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
let onAbort = () => {};
|
|
716
|
+
const aborted = new Promise((_, reject) => {
|
|
717
|
+
onAbort = () => {
|
|
718
|
+
try {
|
|
719
|
+
signal.throwIfAborted();
|
|
720
|
+
} catch (error) {
|
|
721
|
+
reject(error);
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
725
|
+
if (signal.aborted) onAbort();
|
|
726
|
+
});
|
|
727
|
+
try {
|
|
728
|
+
await Promise.race([this.settingsReady, aborted]);
|
|
729
|
+
} finally {
|
|
730
|
+
signal.removeEventListener("abort", onAbort);
|
|
731
|
+
}
|
|
732
|
+
signal.throwIfAborted();
|
|
733
|
+
if (this.settingsFailure) throw this.settingsFailure;
|
|
734
|
+
}
|
|
735
|
+
selectedModel(sessionId) {
|
|
736
|
+
const page = modelRouteOverride(this.settings.model);
|
|
618
737
|
return {
|
|
619
|
-
route: resolveFeatureModel(this.deps.host, page),
|
|
738
|
+
route: resolveFeatureModel(this.deps.host, page, sessionId),
|
|
620
739
|
source: page ? "page" : "default"
|
|
621
740
|
};
|
|
622
741
|
}
|
|
623
742
|
async updateSettings(patch, expectedRevision) {
|
|
624
743
|
const detached = structuredClone(patch);
|
|
625
744
|
const result = this.settingsPending.then(async () => {
|
|
626
|
-
await this.
|
|
745
|
+
await this.ready();
|
|
627
746
|
if (this.disposed) throw new CommitRunError("disposed", "dsh-git-commit: plugin disabled");
|
|
628
747
|
if (expectedRevision !== this.settings.revision) throw new CommitRunError("stale", "dsh-git-commit: settings changed; reload and retry");
|
|
629
748
|
const next = {
|
|
@@ -633,7 +752,10 @@ var GitCommitService = class {
|
|
|
633
752
|
};
|
|
634
753
|
await this.deps.settings?.save(next);
|
|
635
754
|
this.settings = next;
|
|
636
|
-
|
|
755
|
+
try {
|
|
756
|
+
this.deps.onSettingsChanged?.(next.revision);
|
|
757
|
+
} catch {}
|
|
758
|
+
return structuredClone(next);
|
|
637
759
|
});
|
|
638
760
|
this.settingsPending = result.then(() => {}, () => {});
|
|
639
761
|
return result;
|
|
@@ -641,25 +763,30 @@ var GitCommitService = class {
|
|
|
641
763
|
start() {}
|
|
642
764
|
async dispose() {
|
|
643
765
|
this.disposed = true;
|
|
766
|
+
this.lifetime.abort();
|
|
767
|
+
await this.settingsReady;
|
|
644
768
|
await this.settingsPending;
|
|
645
769
|
await this.running?.catch(() => {});
|
|
646
770
|
}
|
|
647
|
-
async resolveRepo(sessionId) {
|
|
771
|
+
async resolveRepo(sessionId, signal) {
|
|
648
772
|
const session = this.deps.sessions.get(sessionId);
|
|
649
773
|
if (!session) throw new CommitRunError("no-session", "dsh-git-commit: session is not live on this host");
|
|
650
774
|
const cwd = normalizeCwd(session.header?.cwd);
|
|
651
775
|
if (!cwd) throw new CommitRunError("no-cwd", "dsh-git-commit: session has no working directory");
|
|
652
776
|
try {
|
|
653
|
-
return await repoInfo(this.run, cwd);
|
|
777
|
+
return await repoInfo(this.run, cwd, signal);
|
|
654
778
|
} catch (error) {
|
|
779
|
+
signal?.throwIfAborted();
|
|
655
780
|
if (isGitMissing(error)) throw new CommitRunError("no-git", "dsh-git-commit: git executable not found");
|
|
656
781
|
throw new CommitRunError("not-a-repo", "dsh-git-commit: session directory is not inside a git repository");
|
|
657
782
|
}
|
|
658
783
|
}
|
|
659
|
-
async status(sessionId) {
|
|
784
|
+
async status(sessionId, signal) {
|
|
785
|
+
const work = this.workSignal(signal);
|
|
786
|
+
await this.waitForSettings(work);
|
|
660
787
|
let resolved;
|
|
661
788
|
try {
|
|
662
|
-
resolved = await this.resolveRepo(sessionId);
|
|
789
|
+
resolved = await this.resolveRepo(sessionId, work);
|
|
663
790
|
} catch (error) {
|
|
664
791
|
if (error instanceof CommitRunError) return {
|
|
665
792
|
available: false,
|
|
@@ -669,8 +796,9 @@ var GitCommitService = class {
|
|
|
669
796
|
};
|
|
670
797
|
throw error;
|
|
671
798
|
}
|
|
672
|
-
const changes = await workingTreeChanges(this.run, resolved.root);
|
|
673
|
-
|
|
799
|
+
const changes = await workingTreeChanges(this.run, resolved.root, work);
|
|
800
|
+
work.throwIfAborted();
|
|
801
|
+
const selected = this.selectedModel(sessionId);
|
|
674
802
|
const model = modelInfo(selected.route, selected.source);
|
|
675
803
|
return {
|
|
676
804
|
available: true,
|
|
@@ -681,19 +809,27 @@ var GitCommitService = class {
|
|
|
681
809
|
};
|
|
682
810
|
}
|
|
683
811
|
async commit(sessionId, signal) {
|
|
812
|
+
if (this.disposed || this.lifetime.signal.aborted) throw new CommitRunError("disposed", "dsh-git-commit: plugin is no longer active");
|
|
684
813
|
if (this.running) throw new CommitRunError("busy", "dsh-git-commit: a commit run is already in progress");
|
|
685
|
-
const
|
|
814
|
+
const work = this.workSignal(signal);
|
|
815
|
+
const run = this.runCommit(sessionId, work);
|
|
686
816
|
this.running = run;
|
|
687
817
|
try {
|
|
688
818
|
return await run;
|
|
819
|
+
} catch (error) {
|
|
820
|
+
if (error instanceof CommitRunError && error.partial) throw error;
|
|
821
|
+
if (work.aborted) throw new CommitRunError("cancelled", "dsh-git-commit: commit run cancelled");
|
|
822
|
+
throw error;
|
|
689
823
|
} finally {
|
|
690
824
|
this.running = void 0;
|
|
691
825
|
}
|
|
692
826
|
}
|
|
693
827
|
async runCommit(sessionId, signal) {
|
|
828
|
+
await this.waitForSettings(signal);
|
|
829
|
+
if (this.disposed || signal?.aborted) throw new CommitRunError("cancelled", "dsh-git-commit: plugin is no longer active");
|
|
694
830
|
const session = this.deps.sessions.get(sessionId);
|
|
695
831
|
const sessionCwd = normalizeCwd(session?.header?.cwd);
|
|
696
|
-
const resolved = await this.resolveRepo(sessionId);
|
|
832
|
+
const resolved = await this.resolveRepo(sessionId, signal);
|
|
697
833
|
const assertReady = () => {
|
|
698
834
|
signal?.throwIfAborted();
|
|
699
835
|
if (this.disposed || !this.deps.llm) throw new CommitRunError("cancelled", "dsh-git-commit: plugin is no longer active");
|
|
@@ -717,11 +853,14 @@ var GitCommitService = class {
|
|
|
717
853
|
const originalVersions = await pathVersions(this.run, resolved.root, paths, signal);
|
|
718
854
|
const [{ stat, diff }, subjects] = await Promise.all([diffAgainstHead(this.run, resolved.root, paths, signal), recentSubjects(this.run, resolved.root, 10, signal)]);
|
|
719
855
|
const untracked = [];
|
|
720
|
-
for (const file of selected)
|
|
856
|
+
for (const file of selected) {
|
|
857
|
+
assertReady();
|
|
858
|
+
if (file.status === "untracked") untracked.push(await this.preview(resolved.root, file.path));
|
|
859
|
+
}
|
|
721
860
|
let groups;
|
|
722
861
|
let planFailure = "no model route was available";
|
|
723
862
|
let model;
|
|
724
|
-
const planned = this.selectedModel();
|
|
863
|
+
const planned = this.selectedModel(sessionId);
|
|
725
864
|
if (planned.route) try {
|
|
726
865
|
const result = await callLlmText(this.deps.llm, {
|
|
727
866
|
plugin: this.deps.plugin,
|
|
@@ -735,13 +874,14 @@ var GitCommitService = class {
|
|
|
735
874
|
untracked
|
|
736
875
|
}),
|
|
737
876
|
maxTokens: DEFAULT_MAX_OUTPUT_TOKENS,
|
|
738
|
-
signal
|
|
877
|
+
signal,
|
|
739
878
|
sessionId
|
|
740
879
|
});
|
|
741
880
|
model = modelInfo(result, planned.source);
|
|
742
881
|
groups = parsePlan(result.text, selected);
|
|
743
882
|
if (!groups) planFailure = "the model returned a plan that does not cover the change set";
|
|
744
883
|
} catch (error) {
|
|
884
|
+
assertReady();
|
|
745
885
|
if (error instanceof CommitRunError) throw error;
|
|
746
886
|
planFailure = error instanceof Error ? error.message : String(error);
|
|
747
887
|
}
|
|
@@ -749,30 +889,59 @@ var GitCommitService = class {
|
|
|
749
889
|
assertReady();
|
|
750
890
|
if ((await workspaceSnapshot(this.run, resolved.root, signal)).fingerprint !== initial.fingerprint) throw new CommitRunError("workspace-changed", "dsh-git-commit: workspace changed during planning; retry the commit");
|
|
751
891
|
const effective = groups ?? [fallbackGroup(selected, looksChinese(subjects) ? "zh" : "en")];
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
892
|
+
const describe = (executed) => {
|
|
893
|
+
const commits = [];
|
|
894
|
+
for (let index = 0; index < executed.length; index += 1) {
|
|
895
|
+
const result = executed[index];
|
|
896
|
+
const group = effective[index];
|
|
897
|
+
if (!result || !group || result.skipped) continue;
|
|
898
|
+
commits.push({
|
|
899
|
+
hash: result.hash,
|
|
900
|
+
message: group.message,
|
|
901
|
+
files: [...group.files]
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
return commits;
|
|
905
|
+
};
|
|
906
|
+
let executed;
|
|
907
|
+
try {
|
|
908
|
+
executed = await commitGroups(this.run, resolved.root, effective, selected, signal, async () => {
|
|
909
|
+
assertReady();
|
|
910
|
+
if (await this.run([
|
|
911
|
+
"symbolic-ref",
|
|
912
|
+
"--quiet",
|
|
913
|
+
"HEAD"
|
|
914
|
+
], {
|
|
915
|
+
cwd: resolved.root,
|
|
916
|
+
signal
|
|
917
|
+
}).catch(() => "DETACHED") !== branchRef) throw new CommitRunError("workspace-changed", "dsh-git-commit: branch changed during the commit run");
|
|
918
|
+
if (await pathVersions(this.run, resolved.root, paths, signal) !== originalVersions) throw new CommitRunError("workspace-changed", "dsh-git-commit: selected file changed during the commit run");
|
|
919
|
+
await workingTreeChanges(this.run, resolved.root, signal);
|
|
920
|
+
});
|
|
921
|
+
} catch (error) {
|
|
922
|
+
if (!(error instanceof PartialCommitError)) throw error;
|
|
923
|
+
const commits = describe(error.commits);
|
|
924
|
+
const cause = error.cause;
|
|
925
|
+
const cancelled = signal?.aborted || typeof cause === "object" && cause !== null && cause.code === "cancelled" || cause instanceof Error && cause.name === "AbortError";
|
|
926
|
+
const code = cause instanceof CommitRunError ? cause.code : cancelled ? "cancelled" : "commit-failed";
|
|
927
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
928
|
+
throw new CommitRunError(code, message, {
|
|
929
|
+
branch: resolved.branch,
|
|
930
|
+
commits,
|
|
931
|
+
fallback: groups === void 0,
|
|
932
|
+
failure: {
|
|
933
|
+
code,
|
|
934
|
+
message
|
|
935
|
+
},
|
|
936
|
+
remaining: effective.slice(error.commits.length + (error.uncertain ? 1 : 0)).map((group) => ({
|
|
937
|
+
message: group.message,
|
|
938
|
+
files: [...group.files]
|
|
939
|
+
})),
|
|
940
|
+
...error.uncertain ? { uncertain: [error.uncertain] } : {},
|
|
941
|
+
...model ? { model } : {}
|
|
774
942
|
});
|
|
775
943
|
}
|
|
944
|
+
const commits = describe(executed);
|
|
776
945
|
if (commits.length === 0) throw new CommitRunError("no-changes", "dsh-git-commit: nothing was committed");
|
|
777
946
|
return {
|
|
778
947
|
branch: resolved.branch,
|
|
@@ -825,7 +994,9 @@ const inject = [
|
|
|
825
994
|
"agents",
|
|
826
995
|
"storageDomain",
|
|
827
996
|
"connection",
|
|
828
|
-
"webServer"
|
|
997
|
+
"webServer",
|
|
998
|
+
"sessionProjections",
|
|
999
|
+
"agentDefaultModel"
|
|
829
1000
|
];
|
|
830
1001
|
function stringField(payload, key) {
|
|
831
1002
|
if (!payload || typeof payload !== "object") return void 0;
|
|
@@ -837,7 +1008,8 @@ function fail(error) {
|
|
|
837
1008
|
ok: false,
|
|
838
1009
|
error: {
|
|
839
1010
|
code: error.code,
|
|
840
|
-
message: error.message
|
|
1011
|
+
message: error.message,
|
|
1012
|
+
...error.partial ? { partial: error.partial } : {}
|
|
841
1013
|
}
|
|
842
1014
|
};
|
|
843
1015
|
return {
|
|
@@ -862,13 +1034,15 @@ async function handleRpc(service, endpoint, payload, signal) {
|
|
|
862
1034
|
};
|
|
863
1035
|
return {
|
|
864
1036
|
ok: true,
|
|
865
|
-
value: await service.status(sessionId)
|
|
1037
|
+
value: await service.status(sessionId, signal)
|
|
866
1038
|
};
|
|
867
1039
|
}
|
|
868
|
-
case "settings":
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1040
|
+
case "settings":
|
|
1041
|
+
await service.ready();
|
|
1042
|
+
return {
|
|
1043
|
+
ok: true,
|
|
1044
|
+
value: service.getSettings()
|
|
1045
|
+
};
|
|
872
1046
|
case "settings.update": {
|
|
873
1047
|
const parsed = updateSettingsSchema.safeParse(payload);
|
|
874
1048
|
if (!parsed.success) return {
|
|
@@ -918,6 +1092,7 @@ function apply(ctx) {
|
|
|
918
1092
|
const tablePromise = domainPromise.then((domain) => domain.table("settings"));
|
|
919
1093
|
const service = new GitCommitService({
|
|
920
1094
|
plugin: PLUGIN_NAME,
|
|
1095
|
+
onSettingsChanged: (revision) => notifyModelSettingsChanged(ctx, PLUGIN_NAME, revision),
|
|
921
1096
|
llm: host.llm,
|
|
922
1097
|
host,
|
|
923
1098
|
sessions: host.sessions,
|
|
@@ -936,6 +1111,43 @@ function apply(ctx) {
|
|
|
936
1111
|
await (await domainPromise).close();
|
|
937
1112
|
}, "git-commit.lifecycle");
|
|
938
1113
|
ctx.effect(() => registerHostRpc(host, RPC_CHANNEL, (endpoint, payload, signal) => handleRpc(service, endpoint, payload, signal)), "git-commit.rpc");
|
|
1114
|
+
ctx.effect(() => registerModelSettings(ctx, {
|
|
1115
|
+
ns: PLUGIN_NAME,
|
|
1116
|
+
title: {
|
|
1117
|
+
zh: "Git 提交",
|
|
1118
|
+
en: "Git Commit"
|
|
1119
|
+
},
|
|
1120
|
+
async read(signal) {
|
|
1121
|
+
await service.ready();
|
|
1122
|
+
signal.throwIfAborted();
|
|
1123
|
+
const settings = service.getSettings();
|
|
1124
|
+
return {
|
|
1125
|
+
revision: settings.revision,
|
|
1126
|
+
fields: [{
|
|
1127
|
+
path: ["model"],
|
|
1128
|
+
label: {
|
|
1129
|
+
zh: "提交模型",
|
|
1130
|
+
en: "Commit model"
|
|
1131
|
+
},
|
|
1132
|
+
current: settings.model,
|
|
1133
|
+
followLabel: {
|
|
1134
|
+
zh: "当前会话 / 宿主默认模型",
|
|
1135
|
+
en: "Current conversation / host default"
|
|
1136
|
+
},
|
|
1137
|
+
allowFollow: true
|
|
1138
|
+
}]
|
|
1139
|
+
};
|
|
1140
|
+
},
|
|
1141
|
+
async write(path, route, expectedRevision, signal) {
|
|
1142
|
+
if (path.length !== 1 || path[0] !== "model") throw new Error("Unknown Git Commit model setting");
|
|
1143
|
+
await service.ready();
|
|
1144
|
+
signal.throwIfAborted();
|
|
1145
|
+
return service.updateSettings({
|
|
1146
|
+
...service.getSettings(),
|
|
1147
|
+
model: route
|
|
1148
|
+
}, expectedRevision);
|
|
1149
|
+
}
|
|
1150
|
+
}), "git-commit.model-settings");
|
|
939
1151
|
}
|
|
940
1152
|
//#endregion
|
|
941
1153
|
export { GitCommitService, apply, buildPlanInput, defaultSettings, handleRpc, inject, name, normalizeFallback, normalizeModelRoute, parsePlan, parsePorcelain };
|