gitxplain 0.1.6 → 0.1.9

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.
@@ -1,4 +1,3 @@
1
- import process from "node:process";
2
1
  import {
3
2
  createCommitFromTree,
4
3
  getCommitMetadata,
@@ -15,19 +14,13 @@ import {
15
14
  listBranchCommits,
16
15
  listCommitsAfter,
17
16
  listTags,
17
+ listTagTargets,
18
18
  localBranchExists,
19
19
  resolveTreeSha,
20
20
  resolveCommitSha,
21
21
  runGitCommand
22
22
  } from "./gitService.js";
23
-
24
- const ANSI = {
25
- reset: "\u001b[0m",
26
- bold: "\u001b[1m",
27
- cyan: "\u001b[36m",
28
- yellow: "\u001b[33m",
29
- green: "\u001b[32m"
30
- };
23
+ import { ANSI, colorize } from "./colorSupport.js";
31
24
 
32
25
  const RELEASE_BRANCH = "release";
33
26
  const VERSION_PATTERN = /\b\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?\b/g;
@@ -36,18 +29,6 @@ const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$
36
29
  const INTEGER_PATTERN = /^\d+$/;
37
30
  const TAG_VERSION_PATTERN = /^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?|\d+)$/;
38
31
 
39
- function supportsColor() {
40
- return Boolean(process.stdout?.isTTY) && process.env.NO_COLOR == null;
41
- }
42
-
43
- function colorize(text, color) {
44
- if (!supportsColor()) {
45
- return text;
46
- }
47
-
48
- return `${color}${text}${ANSI.reset}`;
49
- }
50
-
51
32
  function unique(values) {
52
33
  return [...new Set(values)];
53
34
  }
@@ -306,6 +287,23 @@ export function buildReleaseWindows(sourceCommits) {
306
287
  return windows;
307
288
  }
308
289
 
290
+ function selectLatestWindowsPerVersion(windows) {
291
+ const seenVersions = new Set();
292
+ const latestWindows = [];
293
+
294
+ for (let index = windows.length - 1; index >= 0; index -= 1) {
295
+ const window = windows[index];
296
+ if (!window.version || seenVersions.has(window.version)) {
297
+ continue;
298
+ }
299
+
300
+ seenVersions.add(window.version);
301
+ latestWindows.push(window);
302
+ }
303
+
304
+ return latestWindows.reverse();
305
+ }
306
+
309
307
  export function selectReleaseWindows(sourceCommits, releaseCommits = []) {
310
308
  const windows = buildReleaseWindows(sourceCommits);
311
309
  const releasedVersions = getReleasedVersions(releaseCommits);
@@ -318,21 +316,38 @@ export function selectReleaseWindows(sourceCommits, releaseCommits = []) {
318
316
  };
319
317
  }
320
318
 
321
- export function selectReleaseTags(sourceCommits, existingTagNames = []) {
322
- const windows = buildReleaseWindows(sourceCommits);
319
+ export function selectReleaseTags(sourceCommits, existingTagNames = [], existingTagTargets = []) {
320
+ const windows = selectLatestWindowsPerVersion(buildReleaseWindows(sourceCommits));
323
321
  const taggedVersions = extractTaggedVersions(existingTagNames);
322
+ const targetByVersion = new Map(
323
+ existingTagTargets
324
+ .map((tag) => {
325
+ const version = tag.tagName?.match(TAG_VERSION_PATTERN)?.[1] ?? null;
326
+ return version ? [version, tag.targetSha] : null;
327
+ })
328
+ .filter(Boolean)
329
+ );
324
330
  const tags = windows
325
- .filter((window) => !taggedVersions.has(window.version))
326
331
  .map((window) => {
327
332
  const targetCommit = window.commits.at(-1) ?? null;
333
+ const existingTargetSha = targetByVersion.get(window.version) ?? null;
334
+ const windowCommitShas = new Set(window.commits.map((commit) => commit.sha));
335
+
328
336
  return {
329
337
  ...window,
330
338
  tagName: window.version,
339
+ existingTargetSha,
340
+ needsMove:
341
+ existingTargetSha != null &&
342
+ targetCommit?.sha != null &&
343
+ windowCommitShas.has(existingTargetSha) &&
344
+ existingTargetSha !== targetCommit.sha,
331
345
  targetSha: targetCommit?.sha ?? null,
332
346
  targetShortSha: targetCommit?.shortSha ?? null,
333
347
  targetSubject: targetCommit?.subject ?? null
334
348
  };
335
349
  })
350
+ .filter((tag) => !taggedVersions.has(tag.version) || tag.needsMove)
336
351
  .filter((tag) => tag.targetSha != null);
337
352
 
338
353
  return {
@@ -342,6 +357,32 @@ export function selectReleaseTags(sourceCommits, existingTagNames = []) {
342
357
  };
343
358
  }
344
359
 
360
+ function findLatestTaggedSourceVersion(sourceCommits, taggedVersions) {
361
+ const tagged = new Set(taggedVersions);
362
+ return selectLatestWindowsPerVersion(buildReleaseWindows(sourceCommits))
363
+ .map((window) => window.version)
364
+ .filter((version) => version && tagged.has(version))
365
+ .at(-1) ?? null;
366
+ }
367
+
368
+ function buildReleaseTagPlanForSource(sourceBranch, sourceRef, cwd) {
369
+ const sourceCommits = listBranchCommits(sourceRef, cwd).map((sha) => inspectCommit(sha, cwd));
370
+ const existingTagNames = listTags(cwd);
371
+ const existingTagTargets = listTagTargets(cwd);
372
+ const selection = selectReleaseTags(sourceCommits, existingTagNames, existingTagTargets);
373
+
374
+ return {
375
+ sourceBranch,
376
+ baseRef: sourceRef,
377
+ mergeBase: null,
378
+ releaseExists: localBranchExists(RELEASE_BRANCH, cwd),
379
+ taggedVersions: selection.taggedVersions,
380
+ latestDetectedVersion: selection.latestDetectedVersion,
381
+ latestTaggedVersion: findLatestTaggedSourceVersion(sourceCommits, selection.taggedVersions),
382
+ tags: selection.tags
383
+ };
384
+ }
385
+
345
386
  export function selectReleaseTagsFromReleaseCommits(releaseCommits, existingTagNames = []) {
346
387
  const taggedVersions = extractTaggedVersions(existingTagNames);
347
388
  const tags = releaseCommits
@@ -431,26 +472,7 @@ export function buildReleaseTagPlan(cwd) {
431
472
  throw new Error(`Already on "${RELEASE_BRANCH}". Switch to a source branch before running --tag.`);
432
473
  }
433
474
 
434
- const releaseExists = localBranchExists(RELEASE_BRANCH, cwd);
435
- const baseRef = releaseExists ? RELEASE_BRANCH : getDefaultBaseRef(cwd);
436
- const { mergeBase, sourceCommitShas } = getReleaseTrackSourceCommitShas(releaseExists, baseRef, "HEAD", cwd);
437
- const existingTagNames = listTags(cwd);
438
- const selection = releaseExists
439
- ? selectReleaseTagsFromReleaseCommits(
440
- listBranchCommits(RELEASE_BRANCH, cwd).map((sha) => inspectCommit(sha, cwd)),
441
- existingTagNames
442
- )
443
- : selectReleaseTags(sourceCommits.map((sha) => inspectCommit(sha, cwd)), existingTagNames);
444
-
445
- return {
446
- sourceBranch,
447
- baseRef,
448
- mergeBase,
449
- releaseExists,
450
- taggedVersions: selection.taggedVersions,
451
- latestDetectedVersion: selection.latestDetectedVersion,
452
- tags: selection.tags
453
- };
475
+ return buildReleaseTagPlanForSource(sourceBranch, "HEAD", cwd);
454
476
  }
455
477
 
456
478
  export function finalizeReleaseMergePlan(plan) {
@@ -523,7 +545,11 @@ function buildDriftStatus(sourceRef, sourceLabel, releaseExists, cwd) {
523
545
 
524
546
  function getNextRecommendedAction({ releaseExists, mergePlan, missingTagCount }) {
525
547
  if (!releaseExists && mergePlan.windows.length > 0) {
526
- return `Run \`gitxplain merge --execute\` to create ${RELEASE_BRANCH} and promote ${mergePlan.windows.length} unreleased version(s).`;
548
+ return `Run \`gitxplain --merge --execute\` to create ${RELEASE_BRANCH} and promote ${mergePlan.windows.length} unreleased version(s).`;
549
+ }
550
+
551
+ if (!releaseExists && missingTagCount > 0) {
552
+ return `Run \`gitxplain --tag --execute\` to create ${missingTagCount} missing version tag(s) on the current branch.`;
527
553
  }
528
554
 
529
555
  if (!releaseExists) {
@@ -531,15 +557,15 @@ function getNextRecommendedAction({ releaseExists, mergePlan, missingTagCount })
531
557
  }
532
558
 
533
559
  if (mergePlan.windows.length > 0 && missingTagCount > 0) {
534
- return `Run \`gitxplain merge --execute\` first, then \`gitxplain tag --execute\` to finish tagging release commits.`;
560
+ return `Run \`gitxplain --merge --execute\` to update ${RELEASE_BRANCH}, and \`gitxplain --tag --execute\` to create ${missingTagCount} missing version tag(s).`;
535
561
  }
536
562
 
537
563
  if (mergePlan.windows.length > 0) {
538
- return `Run \`gitxplain merge --execute\` to promote ${mergePlan.windows.length} unreleased version(s) to ${RELEASE_BRANCH}.`;
564
+ return `Run \`gitxplain --merge --execute\` to promote ${mergePlan.windows.length} unreleased version(s) to ${RELEASE_BRANCH}.`;
539
565
  }
540
566
 
541
567
  if (missingTagCount > 0) {
542
- return `Run \`gitxplain tag --execute\` to create ${missingTagCount} missing release tag(s).`;
568
+ return `Run \`gitxplain --tag --execute\` to create ${missingTagCount} missing version tag(s).`;
543
569
  }
544
570
 
545
571
  return "No action required. Release branch and tags are up to date.";
@@ -552,11 +578,9 @@ export function buildReleaseStatus(cwd) {
552
578
  const sourceRef = currentBranch === RELEASE_BRANCH ? sourceBranch : "HEAD";
553
579
  const mergePlan = finalizeReleaseMergePlan(buildReleaseMergePlanForSource(sourceBranch, sourceRef, cwd));
554
580
  const releaseCommits = releaseExists ? listBranchCommits(RELEASE_BRANCH, cwd).map((sha) => inspectCommit(sha, cwd)) : [];
555
- const tagSelection = releaseExists
556
- ? selectReleaseTagsFromReleaseCommits(releaseCommits, listTags(cwd))
557
- : { tags: [], taggedVersions: [], latestDetectedVersion: null };
581
+ const tagPlan = finalizeReleaseTagPlan(buildReleaseTagPlanForSource(sourceBranch, sourceRef, cwd));
558
582
  const drift = buildDriftStatus(sourceRef, sourceBranch, releaseExists, cwd);
559
- const missingTagVersions = tagSelection.tags.map((tag) => tag.tagName);
583
+ const missingTagVersions = tagPlan.tags.map((tag) => tag.tagName);
560
584
  const unmergedVersions = mergePlan.windows.map((window) => window.version);
561
585
 
562
586
  return {
@@ -571,20 +595,12 @@ export function buildReleaseStatus(cwd) {
571
595
  : "healthy",
572
596
  latestSourceVersion: mergePlan.latestDetectedVersion,
573
597
  latestReleaseVersion: findLatestReleaseVersion(releaseCommits),
574
- latestTaggedVersion: findLatestTaggedReleaseVersion(releaseCommits, tagSelection.taggedVersions),
598
+ latestTaggedVersion: tagPlan.latestTaggedVersion,
575
599
  unmergedVersions,
576
600
  missingTagVersions,
577
601
  drift,
578
602
  mergePlan,
579
- tagPlan: finalizeReleaseTagPlan({
580
- sourceBranch,
581
- baseRef: mergePlan.baseRef,
582
- mergeBase: mergePlan.mergeBase,
583
- releaseExists,
584
- taggedVersions: tagSelection.taggedVersions,
585
- latestDetectedVersion: tagSelection.latestDetectedVersion,
586
- tags: tagSelection.tags
587
- }),
603
+ tagPlan,
588
604
  nextRecommendedAction: getNextRecommendedAction({
589
605
  releaseExists,
590
606
  mergePlan,
@@ -638,15 +654,18 @@ export function formatReleaseTagPlan(plan) {
638
654
  ];
639
655
 
640
656
  if (plan.tags.length === 0) {
641
- lines.push(colorize("No unreleased release tags detected. Nothing to tag.", ANSI.green));
657
+ lines.push(colorize("No release tag changes detected. Nothing to tag.", ANSI.green));
642
658
  return lines.join("\n");
643
659
  }
644
660
 
645
661
  for (const tag of plan.tags) {
646
662
  lines.push("");
647
- lines.push(colorize(`tag ${tag.tagName}`, ANSI.bold + ANSI.yellow));
663
+ lines.push(colorize(`${tag.needsMove ? "move tag" : "tag"} ${tag.tagName}`, ANSI.bold + ANSI.yellow));
648
664
  lines.push(`${colorize("Commit Range:", ANSI.bold + ANSI.cyan)} ${tag.startRef}..${tag.endRef}`);
649
665
  lines.push(`${colorize("Target Commit:", ANSI.bold + ANSI.cyan)} ${tag.targetShortSha} ${tag.targetSubject}`);
666
+ if (tag.needsMove) {
667
+ lines.push(`${colorize("Action:", ANSI.bold + ANSI.cyan)} move existing tag to the latest commit for ${tag.tagName}`);
668
+ }
650
669
 
651
670
  for (const commit of tag.commits) {
652
671
  lines.push(`${colorize(commit.shortSha, ANSI.bold + ANSI.cyan)} ${commit.subject}`);
@@ -800,13 +819,17 @@ export function executeReleaseMerge(plan, cwd) {
800
819
 
801
820
  export function executeReleaseTagPlan(plan, cwd) {
802
821
  if (plan.tags.length === 0) {
803
- throw new Error("No unreleased release tags detected. Nothing to tag.");
822
+ throw new Error("No release tag changes detected. Nothing to tag.");
804
823
  }
805
824
 
806
825
  const createdTags = [];
807
826
 
808
827
  try {
809
828
  for (const tag of plan.tags) {
829
+ if (tag.needsMove) {
830
+ gitDeleteTag(tag.tagName, cwd);
831
+ }
832
+
810
833
  gitCreateAnnotatedTag(tag.tagName, tag.targetSha, `release ${tag.tagName}`, cwd);
811
834
  createdTags.push(tag.tagName);
812
835
  }
@@ -1,34 +1,4 @@
1
- import process from "node:process";
2
-
3
- const ANSI = {
4
- reset: "\u001b[0m",
5
- bold: "\u001b[1m",
6
- cyan: "\u001b[36m",
7
- yellow: "\u001b[33m",
8
- green: "\u001b[32m",
9
- red: "\u001b[31m",
10
- gray: "\u001b[90m"
11
- };
12
-
13
- function supportsColor() {
14
- if (process.env.FORCE_COLOR != null && process.env.FORCE_COLOR !== "0") {
15
- return true;
16
- }
17
-
18
- if (process.env.NO_COLOR != null) {
19
- return false;
20
- }
21
-
22
- return Boolean(process.stdout?.isTTY);
23
- }
24
-
25
- function colorize(text, color) {
26
- if (!supportsColor()) {
27
- return text;
28
- }
29
-
30
- return `${color}${text}${ANSI.reset}`;
31
- }
1
+ import { ANSI, colorize } from "./colorSupport.js";
32
2
 
33
3
  function stripInlineMarkdown(text) {
34
4
  return text
@@ -84,61 +54,37 @@ function normalizeMarkdownLine(line, state) {
84
54
  }
85
55
 
86
56
  function formatTargetLabel(commitData) {
87
- return commitData.analysisType === "range" ? "Range" : "Commit";
88
- }
89
-
90
- function normalizeHeading(line) {
91
- const match = line.match(/^([0-9]+\.)?\s*(Summary|Issues? Fixed|Issue|Root Cause|Fix(?: Explanation)?|Impact|Risk Level|Severity|Technical Breakdown|Full Analysis|Line-by-Line Code Walkthrough|Code Review|Security Review|Security Findings|Review Findings|Suggestions|Recommended Mitigations)\s*:?\s*$/i);
92
-
93
- if (!match) {
94
- return null;
57
+ if (commitData.analysisType === "range") {
58
+ return "Range";
95
59
  }
96
60
 
97
- return `${match[2]}:`;
98
- }
99
-
100
- function isFileHeading(line) {
101
- return /^(?:File|Path)\s*:/i.test(line) || /^[A-Za-z0-9_./-]+\.[A-Za-z0-9]+:\s*$/.test(line);
102
- }
103
-
104
- function classifyTone(line) {
105
- if (/^\s*low(?:\b|[.:])/i.test(line)) {
106
- return "good";
61
+ if (commitData.analysisType === "blame") {
62
+ return "File";
107
63
  }
108
64
 
109
- if (/^\s*medium(?:\b|[.:])/i.test(line)) {
110
- return "neutral";
65
+ if (commitData.analysisType === "stash") {
66
+ return "Stash";
111
67
  }
112
68
 
113
- if (/^\s*high(?:\b|[.:])/i.test(line)) {
114
- return "bad";
69
+ if (commitData.analysisType === "conflict") {
70
+ return "Conflict";
115
71
  }
116
72
 
117
- if (
118
- /\b(no significant findings|no security findings|none apparent|looks good|safe|improved|improvement|fixed|resolved|successful|passes?|low risk|low severity)\b/i.test(
119
- line
120
- )
121
- ) {
122
- return "good";
123
- }
73
+ return "Commit";
74
+ }
124
75
 
125
- if (
126
- /\b(issue|issues|bug|broken|failure|failing|risk|risky|severity|vulnerability|insecure|regression|warning|problem|bad|missing|error|high risk|high severity)\b/i.test(
127
- line
128
- )
129
- ) {
130
- return "bad";
131
- }
76
+ function normalizeHeading(line) {
77
+ const match = line.match(/^([0-9]+\.)?\s*(Summary|Issues? Fixed|Issue|Root Cause|Fix(?: Explanation)?|Impact|Risk Level|Severity|Technical Breakdown|Full Analysis|Line-by-Line Code Walkthrough|Code Review|Security Review|Security Findings|Review Findings|Suggestions|Recommended Mitigations)\s*:?\s*$/i);
132
78
 
133
- if (/\b(suggestion|consider|follow-up|todo|medium risk|medium severity)\b/i.test(line)) {
134
- return "neutral";
79
+ if (!match) {
80
+ return null;
135
81
  }
136
82
 
137
- return null;
83
+ return `${match[2]}:`;
138
84
  }
139
85
 
140
- function colorizeByTone(line, tone) {
141
- return line;
86
+ function isFileHeading(line) {
87
+ return /^(?:File|Path)\s*:/i.test(line) || /^[A-Za-z0-9_./-]+\.[A-Za-z0-9]+:\s*$/.test(line);
142
88
  }
143
89
 
144
90
  function formatBulletLine(line) {
@@ -189,7 +135,7 @@ function formatLine(line) {
189
135
  return severityLine;
190
136
  }
191
137
 
192
- return colorizeByTone(line, classifyTone(line));
138
+ return line;
193
139
  }
194
140
 
195
141
  function formatExplanation(explanation) {
@@ -266,6 +212,10 @@ export function formatFooter({ responseMeta, promptMeta, options }) {
266
212
  lines.push(`Usage: ${JSON.stringify(responseMeta.usage)}`);
267
213
  }
268
214
 
215
+ if (responseMeta.estimatedCostUsd != null) {
216
+ lines.push(`Estimated Cost: $${responseMeta.estimatedCostUsd.toFixed(6)}`);
217
+ }
218
+
269
219
  if (promptMeta?.warnings?.length) {
270
220
  lines.push(...promptMeta.warnings);
271
221
  }
@@ -414,6 +414,24 @@ export function inspectRepositoryForPipeline(cwd) {
414
414
  label: "GitHub Actions CI verification",
415
415
  description: "Runs install, lint, test, build, and package checks when supported.",
416
416
  files: [".github/workflows/ci.yml"]
417
+ },
418
+ {
419
+ id: "gitlab-ci",
420
+ label: "GitLab CI verification",
421
+ description: "Creates a .gitlab-ci.yml pipeline with install, lint, test, and build stages.",
422
+ files: [".gitlab-ci.yml"]
423
+ },
424
+ {
425
+ id: "circleci",
426
+ label: "CircleCI verification",
427
+ description: "Creates a .circleci/config.yml pipeline for verification jobs.",
428
+ files: [".circleci/config.yml"]
429
+ },
430
+ {
431
+ id: "bitbucket-pipelines",
432
+ label: "Bitbucket Pipelines verification",
433
+ description: "Creates bitbucket-pipelines.yml with install, test, and build steps.",
434
+ files: ["bitbucket-pipelines.yml"]
417
435
  }
418
436
  ];
419
437
 
@@ -676,6 +694,87 @@ export function buildContainerWorkflow() {
676
694
  ].join("\n").concat("\n");
677
695
  }
678
696
 
697
+ function buildPipelineCommands(context) {
698
+ return [context.commands.install, context.commands.lint, context.commands.test, context.commands.build, context.commands.pack]
699
+ .filter(Boolean);
700
+ }
701
+
702
+ export function buildGitLabCiWorkflow(context) {
703
+ const commands = buildPipelineCommands(context);
704
+ const image =
705
+ context.type === "python"
706
+ ? "python:3.12"
707
+ : context.type === "go"
708
+ ? "golang:1.22"
709
+ : context.type === "rust"
710
+ ? "rust:latest"
711
+ : "node:20";
712
+
713
+ return [
714
+ `image: ${image}`,
715
+ "",
716
+ "stages:",
717
+ " - verify",
718
+ "",
719
+ "verify:",
720
+ " stage: verify",
721
+ " script:",
722
+ ...commands.map((command) => ` - ${command}`)
723
+ ].join("\n").concat("\n");
724
+ }
725
+
726
+ export function buildCircleCiWorkflow(context) {
727
+ const image =
728
+ context.type === "python"
729
+ ? "cimg/python:3.12"
730
+ : context.type === "go"
731
+ ? "cimg/go:1.22"
732
+ : context.type === "rust"
733
+ ? "cimg/rust:1.83"
734
+ : "cimg/node:20.10";
735
+ const commands = buildPipelineCommands(context);
736
+
737
+ return [
738
+ "version: 2.1",
739
+ "",
740
+ "jobs:",
741
+ " verify:",
742
+ " docker:",
743
+ ` - image: ${image}`,
744
+ " steps:",
745
+ " - checkout",
746
+ ...commands.map((command) => ` - run: ${command}`),
747
+ "",
748
+ "workflows:",
749
+ " verify:",
750
+ " jobs:",
751
+ " - verify"
752
+ ].join("\n").concat("\n");
753
+ }
754
+
755
+ export function buildBitbucketPipelinesWorkflow(context) {
756
+ const image =
757
+ context.type === "python"
758
+ ? "python:3.12"
759
+ : context.type === "go"
760
+ ? "golang:1.22"
761
+ : context.type === "rust"
762
+ ? "rust:latest"
763
+ : "node:20";
764
+ const commands = buildPipelineCommands(context);
765
+
766
+ return [
767
+ `image: ${image}`,
768
+ "",
769
+ "pipelines:",
770
+ " default:",
771
+ " - step:",
772
+ " name: Verify",
773
+ " script:",
774
+ ...commands.map((command) => ` - ${command}`)
775
+ ].join("\n").concat("\n");
776
+ }
777
+
679
778
  export function writePipelineFiles(cwd, analysis, selection) {
680
779
  if (!analysis.supported) {
681
780
  throw new Error(analysis.reason);
@@ -689,6 +788,7 @@ export function writePipelineFiles(cwd, analysis, selection) {
689
788
 
690
789
  const writeWorkflow = (relativePath, contents) => {
691
790
  const absolutePath = path.join(cwd, relativePath);
791
+ mkdirSync(path.dirname(absolutePath), { recursive: true });
692
792
  writeFileSync(absolutePath, contents, "utf8");
693
793
  writtenFiles.push(relativePath);
694
794
  };
@@ -713,8 +813,20 @@ export function writePipelineFiles(cwd, analysis, selection) {
713
813
  writeWorkflow(".github/workflows/container.yml", buildContainerWorkflow());
714
814
  }
715
815
 
816
+ if (selection.id === "gitlab-ci") {
817
+ writeWorkflow(".gitlab-ci.yml", buildGitLabCiWorkflow(analysis.primary));
818
+ }
819
+
820
+ if (selection.id === "circleci") {
821
+ writeWorkflow(".circleci/config.yml", buildCircleCiWorkflow(analysis.primary));
822
+ }
823
+
824
+ if (selection.id === "bitbucket-pipelines") {
825
+ writeWorkflow("bitbucket-pipelines.yml", buildBitbucketPipelinesWorkflow(analysis.primary));
826
+ }
827
+
716
828
  if (selection.id === "container" && !selection.files.includes(".github/workflows/ci.yml")) {
717
- notes.push("This option only creates the container workflow. Run `gitxplain pipeline` again if you also want CI verification.");
829
+ notes.push("This option only creates the container workflow. Run `gitxplain --pipeline` again if you also want CI verification.");
718
830
  }
719
831
 
720
832
  return { writtenFiles, notes };
@@ -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);