gitxplain 0.1.8 → 0.2.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/.github/workflows/ci.yml +2 -0
- package/.github/workflows/release.yml +92 -5
- package/README.md +227 -4
- package/cli/index.js +439 -114
- package/cli/services/aiService.js +234 -28
- package/cli/services/cacheService.js +92 -1
- package/cli/services/clipboardService.js +6 -1
- package/cli/services/colorSupport.js +31 -0
- package/cli/services/commitService.js +105 -23
- package/cli/services/configService.js +18 -2
- package/cli/services/envLoader.js +2 -2
- package/cli/services/gitService.js +369 -23
- package/cli/services/hookService.js +36 -4
- package/cli/services/mergeService.js +43 -30
- package/cli/services/outputFormatter.js +23 -73
- package/cli/services/pipelineService.js +344 -9
- package/cli/services/promptService.js +8 -1
- package/cli/services/splitService.js +1 -21
- package/cli/services/usageService.js +158 -0
- package/package.json +4 -4
- package/packaging/README.md +60 -0
- package/packaging/aur/.SRCINFO +12 -0
- package/packaging/aur/PKGBUILD +22 -0
- package/packaging/homebrew-tap/Formula/gitxplain.rb +19 -0
- package/prompts/blame.txt +29 -0
- package/prompts/changelog.txt +36 -0
- package/prompts/conflict.txt +33 -0
- package/prompts/pr-description.txt +40 -0
- package/prompts/refactor.txt +29 -0
- package/prompts/stash.txt +34 -0
- package/prompts/test-suggest.txt +29 -0
- package/scripts/build-deb.sh +64 -0
- package/IMPLEMENTATION.md +0 -225
- package/cli/services/chatService.js +0 -683
- package/cli/services/gitConnectionService.js +0 -267
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
readdirSync,
|
|
6
6
|
writeFileSync
|
|
7
7
|
} from "node:fs";
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
8
9
|
import path from "node:path";
|
|
9
10
|
|
|
10
11
|
const WORKFLOW_DIR = ".github/workflows";
|
|
@@ -64,6 +65,56 @@ function detectNodeVersion(cwd, packageJson) {
|
|
|
64
65
|
};
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
function detectGitHubRepository(cwd) {
|
|
69
|
+
try {
|
|
70
|
+
const remote = execFileSync("git", ["config", "--get", "remote.origin.url"], {
|
|
71
|
+
cwd,
|
|
72
|
+
encoding: "utf8",
|
|
73
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
74
|
+
}).trim();
|
|
75
|
+
|
|
76
|
+
const match =
|
|
77
|
+
remote.match(/github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/i) ??
|
|
78
|
+
remote.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/i);
|
|
79
|
+
|
|
80
|
+
if (!match) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
owner: match[1],
|
|
86
|
+
repo: match[2],
|
|
87
|
+
slug: `${match[1]}/${match[2]}`
|
|
88
|
+
};
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function detectNodePackaging(cwd, packageJson) {
|
|
95
|
+
const packageName = (packageJson?.name ?? "package").replace(/^@[^/]+\//, "");
|
|
96
|
+
const githubRepository = detectGitHubRepository(cwd);
|
|
97
|
+
const homebrewFormulaPath = `packaging/homebrew-tap/Formula/${packageName}.rb`;
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
deb: fileExists(cwd, "scripts/build-deb.sh"),
|
|
101
|
+
aur: fileExists(cwd, "packaging/aur/PKGBUILD"),
|
|
102
|
+
homebrew: fileExists(cwd, homebrewFormulaPath),
|
|
103
|
+
homebrewFormulaPath,
|
|
104
|
+
homebrewTapRepo: githubRepository ? `${githubRepository.owner}/homebrew-tap` : null,
|
|
105
|
+
githubRepository
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function toHomebrewClassName(packageName) {
|
|
110
|
+
return packageName
|
|
111
|
+
.replace(/^@[^/]+\//, "")
|
|
112
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
113
|
+
.filter(Boolean)
|
|
114
|
+
.map((part) => part[0].toUpperCase() + part.slice(1))
|
|
115
|
+
.join("");
|
|
116
|
+
}
|
|
117
|
+
|
|
67
118
|
function detectNodeProject(cwd) {
|
|
68
119
|
if (!fileExists(cwd, "package.json")) {
|
|
69
120
|
return null;
|
|
@@ -75,12 +126,14 @@ function detectNodeProject(cwd) {
|
|
|
75
126
|
const packageManager = detectPackageManager(cwd);
|
|
76
127
|
const releaseSupported = packageJson.private !== true && typeof packageJson.name === "string";
|
|
77
128
|
const packSupported = packageJson.private !== true || Boolean(packageJson.bin);
|
|
129
|
+
const packaging = detectNodePackaging(cwd, packageJson);
|
|
78
130
|
|
|
79
131
|
return {
|
|
80
132
|
type: "node",
|
|
81
133
|
displayName: packageJson.name || path.basename(cwd),
|
|
82
134
|
packageManager,
|
|
83
135
|
packageJson,
|
|
136
|
+
packaging,
|
|
84
137
|
nodeVersion,
|
|
85
138
|
commands: {
|
|
86
139
|
install:
|
|
@@ -277,7 +330,9 @@ function buildNodeSetupStep(nodeVersion, packageManager = "npm") {
|
|
|
277
330
|
" uses: actions/setup-node@v4",
|
|
278
331
|
" with:",
|
|
279
332
|
" node-version-file: .nvmrc",
|
|
280
|
-
` cache: ${packageManager}
|
|
333
|
+
` cache: ${packageManager}`,
|
|
334
|
+
" registry-url: 'https://registry.npmjs.org'",
|
|
335
|
+
" always-auth: true"
|
|
281
336
|
].join("\n");
|
|
282
337
|
}
|
|
283
338
|
|
|
@@ -286,7 +341,9 @@ function buildNodeSetupStep(nodeVersion, packageManager = "npm") {
|
|
|
286
341
|
" uses: actions/setup-node@v4",
|
|
287
342
|
" with:",
|
|
288
343
|
` node-version: '${nodeVersion.value}'`,
|
|
289
|
-
` cache: ${packageManager}
|
|
344
|
+
` cache: ${packageManager}`,
|
|
345
|
+
" registry-url: 'https://registry.npmjs.org'",
|
|
346
|
+
" always-auth: true"
|
|
290
347
|
].join("\n");
|
|
291
348
|
}
|
|
292
349
|
|
|
@@ -414,6 +471,24 @@ export function inspectRepositoryForPipeline(cwd) {
|
|
|
414
471
|
label: "GitHub Actions CI verification",
|
|
415
472
|
description: "Runs install, lint, test, build, and package checks when supported.",
|
|
416
473
|
files: [".github/workflows/ci.yml"]
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
id: "gitlab-ci",
|
|
477
|
+
label: "GitLab CI verification",
|
|
478
|
+
description: "Creates a .gitlab-ci.yml pipeline with install, lint, test, and build stages.",
|
|
479
|
+
files: [".gitlab-ci.yml"]
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
id: "circleci",
|
|
483
|
+
label: "CircleCI verification",
|
|
484
|
+
description: "Creates a .circleci/config.yml pipeline for verification jobs.",
|
|
485
|
+
files: [".circleci/config.yml"]
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
id: "bitbucket-pipelines",
|
|
489
|
+
label: "Bitbucket Pipelines verification",
|
|
490
|
+
description: "Creates bitbucket-pipelines.yml with install, test, and build steps.",
|
|
491
|
+
files: ["bitbucket-pipelines.yml"]
|
|
417
492
|
}
|
|
418
493
|
];
|
|
419
494
|
|
|
@@ -422,8 +497,10 @@ export function inspectRepositoryForPipeline(cwd) {
|
|
|
422
497
|
id: "ci-release",
|
|
423
498
|
label: `CI plus ${primary.release.type} release automation`,
|
|
424
499
|
description:
|
|
425
|
-
primary.type === "node"
|
|
426
|
-
? "Publishes to npm when
|
|
500
|
+
primary.type === "node" && (primary.packaging?.deb || primary.packaging?.homebrew || primary.packaging?.aur)
|
|
501
|
+
? "Publishes to npm on version tags, builds Debian packages, updates Homebrew when configured, and prints AUR update instructions."
|
|
502
|
+
: primary.type === "node"
|
|
503
|
+
? "Publishes to npm when you push a version tag like v1.2.3."
|
|
427
504
|
: primary.type === "python"
|
|
428
505
|
? "Builds and publishes to PyPI when you push a version tag like v1.2.3."
|
|
429
506
|
: "Publishes to crates.io when you push a version tag like v1.2.3.",
|
|
@@ -551,16 +628,154 @@ export function buildReleaseWorkflow(context) {
|
|
|
551
628
|
}
|
|
552
629
|
|
|
553
630
|
if (context.type === "node") {
|
|
631
|
+
const packaging = context.packaging ?? {};
|
|
632
|
+
const githubRepository = packaging.githubRepository?.slug ?? null;
|
|
633
|
+
const homebrewTapRepo = packaging.homebrewTapRepo ?? null;
|
|
634
|
+
const packageName = context.packageJson?.name ?? context.displayName ?? "package";
|
|
635
|
+
const formulaClassName = toHomebrewClassName(packageName);
|
|
636
|
+
const binEntries = Object.entries(context.packageJson?.bin ?? {});
|
|
637
|
+
const executablePath = (binEntries[0]?.[1] ?? "cli/index.js").replace(/^\.\//, "");
|
|
638
|
+
const executableNames = binEntries.length > 0 ? binEntries.map(([name]) => name) : [packageName];
|
|
639
|
+
|
|
640
|
+
if (packaging.deb || packaging.homebrew || packaging.aur) {
|
|
641
|
+
return [
|
|
642
|
+
"name: Release",
|
|
643
|
+
"",
|
|
644
|
+
"on:",
|
|
645
|
+
" push:",
|
|
646
|
+
" tags:",
|
|
647
|
+
" - 'v*'",
|
|
648
|
+
"",
|
|
649
|
+
"permissions:",
|
|
650
|
+
" contents: write",
|
|
651
|
+
"",
|
|
652
|
+
"jobs:",
|
|
653
|
+
" release:",
|
|
654
|
+
" if: startsWith(github.ref_name, 'v')",
|
|
655
|
+
" runs-on: ubuntu-latest",
|
|
656
|
+
"",
|
|
657
|
+
" steps:",
|
|
658
|
+
" - name: Checkout",
|
|
659
|
+
" uses: actions/checkout@v4",
|
|
660
|
+
buildNodeReleaseSetup(context),
|
|
661
|
+
" - name: Derive release metadata",
|
|
662
|
+
" id: meta",
|
|
663
|
+
" run: |",
|
|
664
|
+
" VERSION=\"${GITHUB_REF_NAME#v}\"",
|
|
665
|
+
" echo \"version=${VERSION}\" >> \"${GITHUB_OUTPUT}\"",
|
|
666
|
+
packaging.deb ? ` echo "deb_path=dist/${packageName}_\${VERSION}_all.deb" >> "\${GITHUB_OUTPUT}"` : "",
|
|
667
|
+
context.commands.test ? formatRunStep("Test", context.commands.test) : "",
|
|
668
|
+
context.commands.build ? formatRunStep("Build", context.commands.build) : "",
|
|
669
|
+
packaging.deb ? formatRunStep("Build Debian package", "./scripts/build-deb.sh") : "",
|
|
670
|
+
" - name: Verify npm token",
|
|
671
|
+
" env:",
|
|
672
|
+
" NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}",
|
|
673
|
+
" run: |",
|
|
674
|
+
" if [ -z \"${NODE_AUTH_TOKEN}\" ]; then",
|
|
675
|
+
" echo \"NPM_TOKEN is not configured for this repository.\"",
|
|
676
|
+
" echo \"Add it in GitHub: Settings -> Secrets and variables -> Actions -> New repository secret.\"",
|
|
677
|
+
" exit 1",
|
|
678
|
+
" fi",
|
|
679
|
+
" printf \"//registry.npmjs.org/:_authToken=%s\\n\" \"${NODE_AUTH_TOKEN}\" > \"${HOME}/.npmrc\"",
|
|
680
|
+
" npm whoami",
|
|
681
|
+
" - name: Publish to npm",
|
|
682
|
+
" env:",
|
|
683
|
+
" NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}",
|
|
684
|
+
" run: npm publish",
|
|
685
|
+
" - name: Compute npm tarball SHA-256",
|
|
686
|
+
" id: npm",
|
|
687
|
+
" run: |",
|
|
688
|
+
" VERSION=\"${{ steps.meta.outputs.version }}\"",
|
|
689
|
+
` TARBALL_URL="https://registry.npmjs.org/${packageName}/-/${packageName}-\${VERSION}.tgz"`,
|
|
690
|
+
` curl -fsSL "\${TARBALL_URL}" -o "${packageName}-\${VERSION}.tgz"`,
|
|
691
|
+
` SHA256="$(sha256sum "${packageName}-\${VERSION}.tgz" | awk '{print $1}')"`,
|
|
692
|
+
" echo \"tarball_url=${TARBALL_URL}\" >> \"${GITHUB_OUTPUT}\"",
|
|
693
|
+
" echo \"sha256=${SHA256}\" >> \"${GITHUB_OUTPUT}\"",
|
|
694
|
+
packaging.homebrew && homebrewTapRepo
|
|
695
|
+
? [
|
|
696
|
+
" - name: Checkout Homebrew tap",
|
|
697
|
+
" uses: actions/checkout@v4",
|
|
698
|
+
" with:",
|
|
699
|
+
` repository: ${homebrewTapRepo}`,
|
|
700
|
+
" token: ${{ secrets.HOMEBREW_TAP_TOKEN }}",
|
|
701
|
+
" path: homebrew-tap",
|
|
702
|
+
" - name: Update Homebrew formula",
|
|
703
|
+
" run: |",
|
|
704
|
+
" VERSION=\"${{ steps.meta.outputs.version }}\"",
|
|
705
|
+
" SHA256=\"${{ steps.npm.outputs.sha256 }}\"",
|
|
706
|
+
` FORMULA_PATH="homebrew-tap/Formula/${packageName}.rb"`,
|
|
707
|
+
" mkdir -p \"$(dirname \"${FORMULA_PATH}\")\"",
|
|
708
|
+
" cat > \"${FORMULA_PATH}\" <<EOF",
|
|
709
|
+
` class ${formulaClassName} < Formula`,
|
|
710
|
+
` desc ${JSON.stringify(context.packageJson?.description ?? "")}`,
|
|
711
|
+
` homepage ${JSON.stringify(githubRepository ? `https://github.com/${githubRepository}` : "")}`,
|
|
712
|
+
` url "https://registry.npmjs.org/${packageName}/-/${packageName}-${"${VERSION}"}.tgz"`,
|
|
713
|
+
" sha256 \"${SHA256}\"",
|
|
714
|
+
` license ${JSON.stringify(context.packageJson?.license ?? "MIT")}`,
|
|
715
|
+
"",
|
|
716
|
+
" depends_on \"node\"",
|
|
717
|
+
"",
|
|
718
|
+
" def install",
|
|
719
|
+
" libexec.install Dir[\"package/*\"]",
|
|
720
|
+
...executableNames.map((name) => ` bin.install_symlink libexec/"${executablePath}" => "${name}"`),
|
|
721
|
+
" end",
|
|
722
|
+
"",
|
|
723
|
+
" test do",
|
|
724
|
+
` assert_match ${JSON.stringify(executableNames[0])}, shell_output("#{bin}/${executableNames[0]} --help")`,
|
|
725
|
+
" end",
|
|
726
|
+
" end",
|
|
727
|
+
" EOF",
|
|
728
|
+
" - name: Commit and push Homebrew tap changes",
|
|
729
|
+
" working-directory: homebrew-tap",
|
|
730
|
+
" run: |",
|
|
731
|
+
" git config user.name \"github-actions[bot]\"",
|
|
732
|
+
" git config user.email \"41898282+github-actions[bot]@users.noreply.github.com\"",
|
|
733
|
+
` git add Formula/${packageName}.rb`,
|
|
734
|
+
" if git diff --cached --quiet; then",
|
|
735
|
+
" echo \"No Homebrew formula changes to commit.\"",
|
|
736
|
+
" exit 0",
|
|
737
|
+
" fi",
|
|
738
|
+
" git commit -m \"gitxplain ${GITHUB_REF_NAME}\"",
|
|
739
|
+
" git push"
|
|
740
|
+
].join("\n")
|
|
741
|
+
: "",
|
|
742
|
+
packaging.deb
|
|
743
|
+
? [
|
|
744
|
+
" - name: Create GitHub release and upload Debian package",
|
|
745
|
+
" uses: softprops/action-gh-release@v2",
|
|
746
|
+
" with:",
|
|
747
|
+
" files: ${{ steps.meta.outputs.deb_path }}"
|
|
748
|
+
].join("\n")
|
|
749
|
+
: "",
|
|
750
|
+
packaging.aur
|
|
751
|
+
? [
|
|
752
|
+
" - name: Print AUR update instructions",
|
|
753
|
+
" run: |",
|
|
754
|
+
" VERSION=\"${{ steps.meta.outputs.version }}\"",
|
|
755
|
+
" SHA256=\"${{ steps.npm.outputs.sha256 }}\"",
|
|
756
|
+
" echo \"Manual AUR update steps:\"",
|
|
757
|
+
" echo \"1. Update packaging/aur/PKGBUILD with pkgver=${VERSION} and sha256sums=('${SHA256}').\"",
|
|
758
|
+
" echo \"2. Run: makepkg --printsrcinfo > .SRCINFO\"",
|
|
759
|
+
" echo \"3. Commit PKGBUILD and .SRCINFO to the AUR git repository and push.\""
|
|
760
|
+
].join("\n")
|
|
761
|
+
: ""
|
|
762
|
+
]
|
|
763
|
+
.filter(Boolean)
|
|
764
|
+
.join("\n")
|
|
765
|
+
.concat("\n");
|
|
766
|
+
}
|
|
767
|
+
|
|
554
768
|
return [
|
|
555
769
|
"name: Release",
|
|
556
770
|
"",
|
|
557
771
|
"on:",
|
|
558
772
|
" push:",
|
|
559
773
|
" tags:",
|
|
560
|
-
" - 'v
|
|
774
|
+
" - 'v*'",
|
|
561
775
|
"",
|
|
562
776
|
"jobs:",
|
|
563
777
|
" publish:",
|
|
778
|
+
" if: startsWith(github.ref_name, 'v')",
|
|
564
779
|
" runs-on: ubuntu-latest",
|
|
565
780
|
" permissions:",
|
|
566
781
|
" contents: read",
|
|
@@ -570,6 +785,17 @@ export function buildReleaseWorkflow(context) {
|
|
|
570
785
|
buildNodeReleaseSetup(context),
|
|
571
786
|
context.commands.test ? formatRunStep("Test", context.commands.test) : "",
|
|
572
787
|
context.commands.build ? formatRunStep("Build", context.commands.build) : "",
|
|
788
|
+
" - name: Verify npm token",
|
|
789
|
+
" env:",
|
|
790
|
+
" NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}",
|
|
791
|
+
" run: |",
|
|
792
|
+
" if [ -z \"${NODE_AUTH_TOKEN}\" ]; then",
|
|
793
|
+
" echo \"NPM_TOKEN is not configured for this repository.\"",
|
|
794
|
+
" echo \"Add it in GitHub: Settings -> Secrets and variables -> Actions -> New repository secret.\"",
|
|
795
|
+
" exit 1",
|
|
796
|
+
" fi",
|
|
797
|
+
" printf \"//registry.npmjs.org/:_authToken=%s\\n\" \"${NODE_AUTH_TOKEN}\" > \"${HOME}/.npmrc\"",
|
|
798
|
+
" npm whoami",
|
|
573
799
|
" - name: Publish to npm",
|
|
574
800
|
" env:",
|
|
575
801
|
" NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}",
|
|
@@ -587,7 +813,7 @@ export function buildReleaseWorkflow(context) {
|
|
|
587
813
|
"on:",
|
|
588
814
|
" push:",
|
|
589
815
|
" tags:",
|
|
590
|
-
" - 'v
|
|
816
|
+
" - 'v*'",
|
|
591
817
|
"",
|
|
592
818
|
"jobs:",
|
|
593
819
|
" publish:",
|
|
@@ -617,10 +843,11 @@ export function buildReleaseWorkflow(context) {
|
|
|
617
843
|
"on:",
|
|
618
844
|
" push:",
|
|
619
845
|
" tags:",
|
|
620
|
-
" - 'v
|
|
846
|
+
" - 'v*'",
|
|
621
847
|
"",
|
|
622
848
|
"jobs:",
|
|
623
849
|
" publish:",
|
|
850
|
+
" if: startsWith(github.ref_name, 'v')",
|
|
624
851
|
" runs-on: ubuntu-latest",
|
|
625
852
|
" steps:",
|
|
626
853
|
" - name: Checkout",
|
|
@@ -642,7 +869,7 @@ export function buildContainerWorkflow() {
|
|
|
642
869
|
" push:",
|
|
643
870
|
" branches: [main, master]",
|
|
644
871
|
" tags:",
|
|
645
|
-
" - 'v
|
|
872
|
+
" - 'v*'",
|
|
646
873
|
" pull_request:",
|
|
647
874
|
"",
|
|
648
875
|
"jobs:",
|
|
@@ -676,6 +903,87 @@ export function buildContainerWorkflow() {
|
|
|
676
903
|
].join("\n").concat("\n");
|
|
677
904
|
}
|
|
678
905
|
|
|
906
|
+
function buildPipelineCommands(context) {
|
|
907
|
+
return [context.commands.install, context.commands.lint, context.commands.test, context.commands.build, context.commands.pack]
|
|
908
|
+
.filter(Boolean);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
export function buildGitLabCiWorkflow(context) {
|
|
912
|
+
const commands = buildPipelineCommands(context);
|
|
913
|
+
const image =
|
|
914
|
+
context.type === "python"
|
|
915
|
+
? "python:3.12"
|
|
916
|
+
: context.type === "go"
|
|
917
|
+
? "golang:1.22"
|
|
918
|
+
: context.type === "rust"
|
|
919
|
+
? "rust:latest"
|
|
920
|
+
: "node:20";
|
|
921
|
+
|
|
922
|
+
return [
|
|
923
|
+
`image: ${image}`,
|
|
924
|
+
"",
|
|
925
|
+
"stages:",
|
|
926
|
+
" - verify",
|
|
927
|
+
"",
|
|
928
|
+
"verify:",
|
|
929
|
+
" stage: verify",
|
|
930
|
+
" script:",
|
|
931
|
+
...commands.map((command) => ` - ${command}`)
|
|
932
|
+
].join("\n").concat("\n");
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
export function buildCircleCiWorkflow(context) {
|
|
936
|
+
const image =
|
|
937
|
+
context.type === "python"
|
|
938
|
+
? "cimg/python:3.12"
|
|
939
|
+
: context.type === "go"
|
|
940
|
+
? "cimg/go:1.22"
|
|
941
|
+
: context.type === "rust"
|
|
942
|
+
? "cimg/rust:1.83"
|
|
943
|
+
: "cimg/node:20.10";
|
|
944
|
+
const commands = buildPipelineCommands(context);
|
|
945
|
+
|
|
946
|
+
return [
|
|
947
|
+
"version: 2.1",
|
|
948
|
+
"",
|
|
949
|
+
"jobs:",
|
|
950
|
+
" verify:",
|
|
951
|
+
" docker:",
|
|
952
|
+
` - image: ${image}`,
|
|
953
|
+
" steps:",
|
|
954
|
+
" - checkout",
|
|
955
|
+
...commands.map((command) => ` - run: ${command}`),
|
|
956
|
+
"",
|
|
957
|
+
"workflows:",
|
|
958
|
+
" verify:",
|
|
959
|
+
" jobs:",
|
|
960
|
+
" - verify"
|
|
961
|
+
].join("\n").concat("\n");
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
export function buildBitbucketPipelinesWorkflow(context) {
|
|
965
|
+
const image =
|
|
966
|
+
context.type === "python"
|
|
967
|
+
? "python:3.12"
|
|
968
|
+
: context.type === "go"
|
|
969
|
+
? "golang:1.22"
|
|
970
|
+
: context.type === "rust"
|
|
971
|
+
? "rust:latest"
|
|
972
|
+
: "node:20";
|
|
973
|
+
const commands = buildPipelineCommands(context);
|
|
974
|
+
|
|
975
|
+
return [
|
|
976
|
+
`image: ${image}`,
|
|
977
|
+
"",
|
|
978
|
+
"pipelines:",
|
|
979
|
+
" default:",
|
|
980
|
+
" - step:",
|
|
981
|
+
" name: Verify",
|
|
982
|
+
" script:",
|
|
983
|
+
...commands.map((command) => ` - ${command}`)
|
|
984
|
+
].join("\n").concat("\n");
|
|
985
|
+
}
|
|
986
|
+
|
|
679
987
|
export function writePipelineFiles(cwd, analysis, selection) {
|
|
680
988
|
if (!analysis.supported) {
|
|
681
989
|
throw new Error(analysis.reason);
|
|
@@ -685,12 +993,21 @@ export function writePipelineFiles(cwd, analysis, selection) {
|
|
|
685
993
|
mkdirSync(workflowDir, { recursive: true });
|
|
686
994
|
|
|
687
995
|
const writtenFiles = [];
|
|
996
|
+
const updatedFiles = [];
|
|
997
|
+
const unchangedFiles = [];
|
|
688
998
|
const notes = [];
|
|
689
999
|
|
|
690
1000
|
const writeWorkflow = (relativePath, contents) => {
|
|
691
1001
|
const absolutePath = path.join(cwd, relativePath);
|
|
1002
|
+
mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
1003
|
+
const existingContents = existsSync(absolutePath) ? readFileSync(absolutePath, "utf8") : null;
|
|
692
1004
|
writeFileSync(absolutePath, contents, "utf8");
|
|
693
1005
|
writtenFiles.push(relativePath);
|
|
1006
|
+
if (existingContents === contents) {
|
|
1007
|
+
unchangedFiles.push(relativePath);
|
|
1008
|
+
} else {
|
|
1009
|
+
updatedFiles.push(relativePath);
|
|
1010
|
+
}
|
|
694
1011
|
};
|
|
695
1012
|
|
|
696
1013
|
if (selection.id === "ci" || selection.id === "ci-release") {
|
|
@@ -702,6 +1019,12 @@ export function writePipelineFiles(cwd, analysis, selection) {
|
|
|
702
1019
|
|
|
703
1020
|
if (analysis.primary.release.type === "npm") {
|
|
704
1021
|
notes.push("Add an `NPM_TOKEN` repository secret before pushing a release tag.");
|
|
1022
|
+
if (analysis.primary.packaging?.homebrew) {
|
|
1023
|
+
notes.push("Add a `HOMEBREW_TAP_TOKEN` repository secret so CI can update your tap repository.");
|
|
1024
|
+
}
|
|
1025
|
+
if (analysis.primary.packaging?.aur) {
|
|
1026
|
+
notes.push("AUR updates are still manual. The generated release workflow prints the exact PKGBUILD and .SRCINFO refresh steps.");
|
|
1027
|
+
}
|
|
705
1028
|
} else if (analysis.primary.release.type === "pypi") {
|
|
706
1029
|
notes.push("Add a `PYPI_TOKEN` repository secret before pushing a release tag.");
|
|
707
1030
|
} else if (analysis.primary.release.type === "crates") {
|
|
@@ -713,9 +1036,21 @@ export function writePipelineFiles(cwd, analysis, selection) {
|
|
|
713
1036
|
writeWorkflow(".github/workflows/container.yml", buildContainerWorkflow());
|
|
714
1037
|
}
|
|
715
1038
|
|
|
1039
|
+
if (selection.id === "gitlab-ci") {
|
|
1040
|
+
writeWorkflow(".gitlab-ci.yml", buildGitLabCiWorkflow(analysis.primary));
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
if (selection.id === "circleci") {
|
|
1044
|
+
writeWorkflow(".circleci/config.yml", buildCircleCiWorkflow(analysis.primary));
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
if (selection.id === "bitbucket-pipelines") {
|
|
1048
|
+
writeWorkflow("bitbucket-pipelines.yml", buildBitbucketPipelinesWorkflow(analysis.primary));
|
|
1049
|
+
}
|
|
1050
|
+
|
|
716
1051
|
if (selection.id === "container" && !selection.files.includes(".github/workflows/ci.yml")) {
|
|
717
1052
|
notes.push("This option only creates the container workflow. Run `gitxplain --pipeline` again if you also want CI verification.");
|
|
718
1053
|
}
|
|
719
1054
|
|
|
720
|
-
return { writtenFiles, notes };
|
|
1055
|
+
return { writtenFiles, updatedFiles, unchangedFiles, notes };
|
|
721
1056
|
}
|
|
@@ -16,7 +16,14 @@ const PROMPT_FILES = {
|
|
|
16
16
|
review: "review.txt",
|
|
17
17
|
security: "security.txt",
|
|
18
18
|
split: "split.txt",
|
|
19
|
-
commit: "commit.txt"
|
|
19
|
+
commit: "commit.txt",
|
|
20
|
+
changelog: "changelog.txt",
|
|
21
|
+
refactor: "refactor.txt",
|
|
22
|
+
"test-suggest": "test-suggest.txt",
|
|
23
|
+
"pr-description": "pr-description.txt",
|
|
24
|
+
blame: "blame.txt",
|
|
25
|
+
stash: "stash.txt",
|
|
26
|
+
conflict: "conflict.txt"
|
|
20
27
|
};
|
|
21
28
|
|
|
22
29
|
function fillTemplate(template, values) {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import process from "node:process";
|
|
2
1
|
import {
|
|
3
2
|
createCommitFromTree,
|
|
4
3
|
deletePaths,
|
|
@@ -32,26 +31,7 @@ import {
|
|
|
32
31
|
runGitCommandUnchecked,
|
|
33
32
|
resolveCommitSha
|
|
34
33
|
} from "./gitService.js";
|
|
35
|
-
|
|
36
|
-
const ANSI = {
|
|
37
|
-
reset: "\u001b[0m",
|
|
38
|
-
bold: "\u001b[1m",
|
|
39
|
-
cyan: "\u001b[36m",
|
|
40
|
-
yellow: "\u001b[33m",
|
|
41
|
-
green: "\u001b[32m"
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
function supportsColor() {
|
|
45
|
-
return Boolean(process.stdout?.isTTY) && process.env.NO_COLOR == null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function colorize(text, color) {
|
|
49
|
-
if (!supportsColor()) {
|
|
50
|
-
return text;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return `${color}${text}${ANSI.reset}`;
|
|
54
|
-
}
|
|
34
|
+
import { ANSI, colorize } from "./colorSupport.js";
|
|
55
35
|
|
|
56
36
|
function extractJsonPayload(explanation) {
|
|
57
37
|
const fencedMatch = explanation.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
function getUsageLogPath() {
|
|
6
|
+
return path.join(os.homedir(), ".gitxplain", "usage.jsonl");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getUsageLogFile() {
|
|
10
|
+
return getUsageLogPath();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function readRecords() {
|
|
14
|
+
const filePath = getUsageLogPath();
|
|
15
|
+
if (!existsSync(filePath)) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return readFileSync(filePath, "utf8")
|
|
20
|
+
.split("\n")
|
|
21
|
+
.map((line) => line.trim())
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
.map((line) => JSON.parse(line));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseNumeric(value) {
|
|
27
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function normalizeUsageMetrics(usage) {
|
|
31
|
+
if (!usage || typeof usage !== "object") {
|
|
32
|
+
return {
|
|
33
|
+
inputTokens: 0,
|
|
34
|
+
outputTokens: 0,
|
|
35
|
+
totalTokens: 0
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const inputTokens =
|
|
40
|
+
parseNumeric(usage.prompt_tokens) ||
|
|
41
|
+
parseNumeric(usage.input_tokens) ||
|
|
42
|
+
parseNumeric(usage.promptTokenCount);
|
|
43
|
+
const outputTokens =
|
|
44
|
+
parseNumeric(usage.completion_tokens) ||
|
|
45
|
+
parseNumeric(usage.output_tokens) ||
|
|
46
|
+
parseNumeric(usage.candidatesTokenCount);
|
|
47
|
+
const totalTokens =
|
|
48
|
+
parseNumeric(usage.total_tokens) ||
|
|
49
|
+
parseNumeric(usage.totalTokenCount) ||
|
|
50
|
+
inputTokens + outputTokens;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
inputTokens,
|
|
54
|
+
outputTokens,
|
|
55
|
+
totalTokens
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseEnvPrice(envKey) {
|
|
60
|
+
const raw = process.env[envKey];
|
|
61
|
+
if (raw == null || raw === "") {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const parsed = Number.parseFloat(raw);
|
|
66
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function resolvePricing(config) {
|
|
70
|
+
const providerKey = config.provider.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
|
|
71
|
+
const modelKey = String(config.model ?? "default").toUpperCase().replace(/[^A-Z0-9]+/g, "_");
|
|
72
|
+
|
|
73
|
+
const inputPerMillion =
|
|
74
|
+
parseEnvPrice(`${providerKey}_${modelKey}_INPUT_COST_PER_MTOK`) ??
|
|
75
|
+
parseEnvPrice(`${providerKey}_INPUT_COST_PER_MTOK`) ??
|
|
76
|
+
parseEnvPrice("LLM_INPUT_COST_PER_MTOK");
|
|
77
|
+
const outputPerMillion =
|
|
78
|
+
parseEnvPrice(`${providerKey}_${modelKey}_OUTPUT_COST_PER_MTOK`) ??
|
|
79
|
+
parseEnvPrice(`${providerKey}_OUTPUT_COST_PER_MTOK`) ??
|
|
80
|
+
parseEnvPrice("LLM_OUTPUT_COST_PER_MTOK");
|
|
81
|
+
|
|
82
|
+
if (inputPerMillion == null || outputPerMillion == null) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
inputPerMillion,
|
|
88
|
+
outputPerMillion
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function estimateCostUsd(usage, pricing) {
|
|
93
|
+
if (!pricing) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const metrics = normalizeUsageMetrics(usage);
|
|
98
|
+
const costUsd =
|
|
99
|
+
(metrics.inputTokens / 1_000_000) * pricing.inputPerMillion +
|
|
100
|
+
(metrics.outputTokens / 1_000_000) * pricing.outputPerMillion;
|
|
101
|
+
|
|
102
|
+
return Number.isFinite(costUsd) ? costUsd : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function appendUsageRecord({ provider, model, usage, latencyMs, estimatedCostUsd }) {
|
|
106
|
+
const metrics = normalizeUsageMetrics(usage);
|
|
107
|
+
if (metrics.totalTokens === 0 && estimatedCostUsd == null) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const filePath = getUsageLogPath();
|
|
112
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
113
|
+
appendFileSync(
|
|
114
|
+
filePath,
|
|
115
|
+
`${JSON.stringify({
|
|
116
|
+
timestamp: new Date().toISOString(),
|
|
117
|
+
provider,
|
|
118
|
+
model,
|
|
119
|
+
usage: metrics,
|
|
120
|
+
latencyMs: latencyMs ?? null,
|
|
121
|
+
estimatedCostUsd
|
|
122
|
+
})}\n`,
|
|
123
|
+
"utf8"
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function getUsageStats() {
|
|
128
|
+
const records = readRecords();
|
|
129
|
+
|
|
130
|
+
return records.reduce(
|
|
131
|
+
(summary, record) => {
|
|
132
|
+
summary.requestCount += 1;
|
|
133
|
+
summary.inputTokens += parseNumeric(record.usage?.inputTokens);
|
|
134
|
+
summary.outputTokens += parseNumeric(record.usage?.outputTokens);
|
|
135
|
+
summary.totalTokens += parseNumeric(record.usage?.totalTokens);
|
|
136
|
+
summary.estimatedCostUsd += parseNumeric(record.estimatedCostUsd);
|
|
137
|
+
return summary;
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
requestCount: 0,
|
|
141
|
+
inputTokens: 0,
|
|
142
|
+
outputTokens: 0,
|
|
143
|
+
totalTokens: 0,
|
|
144
|
+
estimatedCostUsd: 0
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function clearUsageLog() {
|
|
150
|
+
const filePath = getUsageLogPath();
|
|
151
|
+
const count = readRecords().length;
|
|
152
|
+
|
|
153
|
+
if (existsSync(filePath)) {
|
|
154
|
+
rmSync(filePath, { force: true });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return count;
|
|
158
|
+
}
|