threadlines 0.2.4 → 0.2.6

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.
@@ -44,6 +44,7 @@ const config_1 = require("../utils/config");
44
44
  const environment_1 = require("../utils/environment");
45
45
  const github_1 = require("../git/github");
46
46
  const gitlab_1 = require("../git/gitlab");
47
+ const bitbucket_1 = require("../git/bitbucket");
47
48
  const vercel_1 = require("../git/vercel");
48
49
  const local_1 = require("../git/local");
49
50
  const diff_1 = require("../git/diff");
@@ -51,6 +52,24 @@ const fs = __importStar(require("fs"));
51
52
  const path = __importStar(require("path"));
52
53
  const chalk_1 = __importDefault(require("chalk"));
53
54
  const simple_git_1 = __importDefault(require("simple-git"));
55
+ /**
56
+ * Helper to get context for any environment.
57
+ * This centralizes the environment switch logic.
58
+ */
59
+ async function getContextForEnvironment(environment, repoRoot, commitSha) {
60
+ switch (environment) {
61
+ case 'github':
62
+ return (0, github_1.getGitHubContext)(repoRoot);
63
+ case 'gitlab':
64
+ return (0, gitlab_1.getGitLabContext)(repoRoot);
65
+ case 'bitbucket':
66
+ return (0, bitbucket_1.getBitbucketContext)(repoRoot);
67
+ case 'vercel':
68
+ return (0, vercel_1.getVercelContext)(repoRoot);
69
+ default:
70
+ return (0, local_1.getLocalContext)(repoRoot, commitSha);
71
+ }
72
+ }
54
73
  // Get CLI version from package.json
55
74
  const packageJsonPath = path.join(__dirname, '../../package.json');
56
75
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
@@ -103,9 +122,10 @@ async function checkCommand(options) {
103
122
  console.log(chalk_1.default.gray(' 3. Make sure .env.local is in your .gitignore'));
104
123
  console.log('');
105
124
  console.log(chalk_1.default.white(' CI/CD:'));
106
- console.log(chalk_1.default.gray(' GitHub Actions: Settings → Secrets → Add variables'));
107
- console.log(chalk_1.default.gray(' GitLab CI: Settings → CI/CD → Variables'));
108
- console.log(chalk_1.default.gray(' Vercel: SettingsEnvironment Variables'));
125
+ console.log(chalk_1.default.gray(' GitHub Actions: Settings → Secrets → Add variables'));
126
+ console.log(chalk_1.default.gray(' GitLab CI: Settings → CI/CD → Variables'));
127
+ console.log(chalk_1.default.gray(' Bitbucket Pipelines: Repository settings Repository variables'));
128
+ console.log(chalk_1.default.gray(' Vercel: Settings → Environment Variables'));
109
129
  console.log('');
110
130
  console.log(chalk_1.default.gray('Get your credentials at: https://devthreadline.com/settings'));
111
131
  process.exit(1);
@@ -126,155 +146,37 @@ async function checkCommand(options) {
126
146
  let repoName;
127
147
  let branchName;
128
148
  let metadata = {};
129
- // Validate mutually exclusive flags
149
+ // Check for explicit flags
130
150
  const explicitFlags = [options.branch, options.commit, options.file, options.folder, options.files].filter(Boolean);
151
+ // Validate mutually exclusive flags
131
152
  if (explicitFlags.length > 1) {
132
153
  console.error(chalk_1.default.red('❌ Error: Only one review option can be specified at a time'));
133
154
  console.log(chalk_1.default.gray(' Options: --branch, --commit, --file, --folder, --files'));
134
155
  process.exit(1);
135
156
  }
136
- // Check for explicit flags first (override auto-detection)
137
- if (options.file) {
138
- console.log(chalk_1.default.gray(`📝 Reading file: ${options.file}...`));
139
- gitDiff = await (0, file_1.getFileContent)(repoRoot, options.file);
140
- }
141
- else if (options.folder) {
142
- console.log(chalk_1.default.gray(`📝 Reading folder: ${options.folder}...`));
143
- gitDiff = await (0, file_1.getFolderContent)(repoRoot, options.folder);
144
- }
145
- else if (options.files && options.files.length > 0) {
146
- console.log(chalk_1.default.gray(`📝 Reading ${options.files.length} file(s)...`));
147
- gitDiff = await (0, file_1.getMultipleFilesContent)(repoRoot, options.files);
148
- }
149
- else if (options.branch) {
150
- console.log(chalk_1.default.gray(`📝 Collecting git changes for branch: ${options.branch}...`));
151
- gitDiff = await (0, diff_1.getBranchDiff)(repoRoot, options.branch);
152
- // Get repo/branch using environment-specific approach
153
- if (environment === 'github') {
154
- const gitContext = await (0, github_1.getGitHubContext)(repoRoot);
155
- repoName = gitContext.repoName;
156
- branchName = gitContext.branchName;
157
- metadata = {
158
- commitSha: gitContext.commitSha,
159
- commitMessage: gitContext.commitMessage,
160
- commitAuthorName: gitContext.commitAuthor.name,
161
- commitAuthorEmail: gitContext.commitAuthor.email,
162
- prTitle: gitContext.prTitle
163
- };
164
- }
165
- else if (environment === 'gitlab') {
166
- const gitContext = await (0, gitlab_1.getGitLabContext)(repoRoot);
167
- repoName = gitContext.repoName;
168
- branchName = gitContext.branchName;
169
- metadata = {
170
- commitSha: gitContext.commitSha,
171
- commitMessage: gitContext.commitMessage,
172
- commitAuthorName: gitContext.commitAuthor.name,
173
- commitAuthorEmail: gitContext.commitAuthor.email,
174
- prTitle: gitContext.prTitle
175
- };
176
- }
177
- else if (environment === 'vercel') {
178
- const gitContext = await (0, vercel_1.getVercelContext)(repoRoot);
179
- repoName = gitContext.repoName;
180
- branchName = gitContext.branchName;
181
- metadata = {
182
- commitSha: gitContext.commitSha,
183
- commitMessage: gitContext.commitMessage,
184
- commitAuthorName: gitContext.commitAuthor.name,
185
- commitAuthorEmail: gitContext.commitAuthor.email
186
- };
187
- }
188
- else {
189
- const gitContext = await (0, local_1.getLocalContext)(repoRoot);
190
- repoName = gitContext.repoName;
191
- branchName = gitContext.branchName;
192
- metadata = {
193
- commitSha: gitContext.commitSha,
194
- commitMessage: gitContext.commitMessage,
195
- commitAuthorName: gitContext.commitAuthor.name,
196
- commitAuthorEmail: gitContext.commitAuthor.email
197
- };
198
- }
199
- }
200
- else if (options.commit) {
201
- console.log(chalk_1.default.gray(`📝 Collecting git changes for commit: ${options.commit}...`));
202
- gitDiff = await (0, diff_1.getCommitDiff)(repoRoot, options.commit);
203
- // Get repo/branch using environment-specific approach
204
- if (environment === 'github') {
205
- const gitContext = await (0, github_1.getGitHubContext)(repoRoot);
206
- repoName = gitContext.repoName;
207
- branchName = gitContext.branchName;
208
- metadata = {
209
- commitSha: gitContext.commitSha,
210
- commitMessage: gitContext.commitMessage,
211
- commitAuthorName: gitContext.commitAuthor.name,
212
- commitAuthorEmail: gitContext.commitAuthor.email,
213
- prTitle: gitContext.prTitle
214
- };
157
+ // CI environments: auto-detect only, flags are ignored with warning
158
+ // Local: full flag support for developer flexibility
159
+ if ((0, environment_1.isCIEnvironment)(environment)) {
160
+ // Warn if flags are passed in CI - they're meant for local development
161
+ if (explicitFlags.length > 0) {
162
+ const flagName = options.branch ? '--branch' :
163
+ options.commit ? '--commit' :
164
+ options.file ? '--file' :
165
+ options.folder ? '--folder' : '--files';
166
+ console.log(chalk_1.default.yellow(`⚠️ Warning: ${flagName} flag ignored in CI environment. Using auto-detection.\n`));
215
167
  }
216
- else if (environment === 'gitlab') {
217
- const gitContext = await (0, gitlab_1.getGitLabContext)(repoRoot);
218
- repoName = gitContext.repoName;
219
- branchName = gitContext.branchName;
220
- metadata = {
221
- commitSha: gitContext.commitSha,
222
- commitMessage: gitContext.commitMessage,
223
- commitAuthorName: gitContext.commitAuthor.name,
224
- commitAuthorEmail: gitContext.commitAuthor.email,
225
- prTitle: gitContext.prTitle
226
- };
227
- }
228
- else if (environment === 'vercel') {
229
- const gitContext = await (0, vercel_1.getVercelContext)(repoRoot);
230
- repoName = gitContext.repoName;
231
- branchName = gitContext.branchName;
232
- metadata = {
233
- commitSha: gitContext.commitSha,
234
- commitMessage: gitContext.commitMessage,
235
- commitAuthorName: gitContext.commitAuthor.name,
236
- commitAuthorEmail: gitContext.commitAuthor.email
237
- };
238
- }
239
- else {
240
- const gitContext = await (0, local_1.getLocalContext)(repoRoot, options.commit);
241
- repoName = gitContext.repoName;
242
- branchName = gitContext.branchName;
243
- metadata = {
244
- commitSha: gitContext.commitSha,
245
- commitMessage: gitContext.commitMessage,
246
- commitAuthorName: gitContext.commitAuthor.name,
247
- commitAuthorEmail: gitContext.commitAuthor.email
248
- };
249
- }
250
- }
251
- else {
252
- // Auto-detect: Use environment-specific context collection (completely isolated)
168
+ // CI auto-detect: use environment-specific context
253
169
  const envNames = {
254
170
  vercel: 'Vercel',
255
- github: 'GitHub',
256
- gitlab: 'GitLab',
257
- local: 'Local'
171
+ github: 'GitHub Actions',
172
+ gitlab: 'GitLab CI',
173
+ bitbucket: 'Bitbucket Pipelines'
258
174
  };
259
175
  console.log(chalk_1.default.gray(`📝 Collecting git context for ${envNames[environment]}...`));
260
- // Get all context from environment-specific module
261
- let envContext;
262
- if (environment === 'github') {
263
- envContext = await (0, github_1.getGitHubContext)(repoRoot);
264
- }
265
- else if (environment === 'gitlab') {
266
- envContext = await (0, gitlab_1.getGitLabContext)(repoRoot);
267
- }
268
- else if (environment === 'vercel') {
269
- envContext = await (0, vercel_1.getVercelContext)(repoRoot);
270
- }
271
- else {
272
- envContext = await (0, local_1.getLocalContext)(repoRoot);
273
- }
176
+ const envContext = await getContextForEnvironment(environment, repoRoot);
274
177
  gitDiff = envContext.diff;
275
178
  repoName = envContext.repoName;
276
179
  branchName = envContext.branchName;
277
- // Use metadata from environment context
278
180
  metadata = {
279
181
  commitSha: envContext.commitSha,
280
182
  commitMessage: envContext.commitMessage,
@@ -283,6 +185,63 @@ async function checkCommand(options) {
283
185
  prTitle: envContext.prTitle
284
186
  };
285
187
  }
188
+ else {
189
+ // Local environment: support all flags
190
+ if (options.file) {
191
+ console.log(chalk_1.default.gray(`📝 Reading file: ${options.file}...`));
192
+ gitDiff = await (0, file_1.getFileContent)(repoRoot, options.file);
193
+ }
194
+ else if (options.folder) {
195
+ console.log(chalk_1.default.gray(`📝 Reading folder: ${options.folder}...`));
196
+ gitDiff = await (0, file_1.getFolderContent)(repoRoot, options.folder);
197
+ }
198
+ else if (options.files && options.files.length > 0) {
199
+ console.log(chalk_1.default.gray(`📝 Reading ${options.files.length} file(s)...`));
200
+ gitDiff = await (0, file_1.getMultipleFilesContent)(repoRoot, options.files);
201
+ }
202
+ else if (options.branch) {
203
+ console.log(chalk_1.default.gray(`📝 Collecting git changes for branch: ${options.branch}...`));
204
+ gitDiff = await (0, diff_1.getBranchDiff)(repoRoot, options.branch);
205
+ // Use local context for metadata
206
+ const localContext = await (0, local_1.getLocalContext)(repoRoot);
207
+ repoName = localContext.repoName;
208
+ branchName = localContext.branchName;
209
+ metadata = {
210
+ commitSha: localContext.commitSha,
211
+ commitMessage: localContext.commitMessage,
212
+ commitAuthorName: localContext.commitAuthor.name,
213
+ commitAuthorEmail: localContext.commitAuthor.email
214
+ };
215
+ }
216
+ else if (options.commit) {
217
+ console.log(chalk_1.default.gray(`📝 Collecting git changes for commit: ${options.commit}...`));
218
+ gitDiff = await (0, diff_1.getCommitDiff)(repoRoot, options.commit);
219
+ // Use local context for metadata, passing commit SHA for author lookup
220
+ const localContext = await (0, local_1.getLocalContext)(repoRoot, options.commit);
221
+ repoName = localContext.repoName;
222
+ branchName = localContext.branchName;
223
+ metadata = {
224
+ commitSha: localContext.commitSha,
225
+ commitMessage: localContext.commitMessage,
226
+ commitAuthorName: localContext.commitAuthor.name,
227
+ commitAuthorEmail: localContext.commitAuthor.email
228
+ };
229
+ }
230
+ else {
231
+ // Local auto-detect: staged/unstaged changes
232
+ console.log(chalk_1.default.gray('📝 Collecting git context for Local...'));
233
+ const localContext = await (0, local_1.getLocalContext)(repoRoot);
234
+ gitDiff = localContext.diff;
235
+ repoName = localContext.repoName;
236
+ branchName = localContext.branchName;
237
+ metadata = {
238
+ commitSha: localContext.commitSha,
239
+ commitMessage: localContext.commitMessage,
240
+ commitAuthorName: localContext.commitAuthor.name,
241
+ commitAuthorEmail: localContext.commitAuthor.email
242
+ };
243
+ }
244
+ }
286
245
  if (gitDiff.changedFiles.length === 0) {
287
246
  console.error(chalk_1.default.bold('ℹ️ No changes detected.'));
288
247
  process.exit(0);
@@ -0,0 +1,259 @@
1
+ "use strict";
2
+ /**
3
+ * Bitbucket Pipelines Environment - Complete Isolation
4
+ *
5
+ * All Bitbucket-specific logic is contained in this file.
6
+ * No dependencies on other environment implementations.
7
+ *
8
+ * Exports a single function: getBitbucketContext() that returns:
9
+ * - diff: GitDiffResult
10
+ * - repoName: string
11
+ * - branchName: string
12
+ * - commitAuthor: { name: string; email: string }
13
+ * - prTitle?: string (PR title - not available in Bitbucket env vars)
14
+ *
15
+ * Implementation Status (all tested 2026-01-18):
16
+ * - ✅ Direct commit to main
17
+ * - ✅ Feature branch push
18
+ * - ✅ PR context
19
+ */
20
+ var __importDefault = (this && this.__importDefault) || function (mod) {
21
+ return (mod && mod.__esModule) ? mod : { "default": mod };
22
+ };
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.getBitbucketContext = getBitbucketContext;
25
+ const simple_git_1 = __importDefault(require("simple-git"));
26
+ const child_process_1 = require("child_process");
27
+ const diff_1 = require("./diff");
28
+ /**
29
+ * Gets all Bitbucket context in one call
30
+ */
31
+ async function getBitbucketContext(repoRoot) {
32
+ const git = (0, simple_git_1.default)(repoRoot);
33
+ // Check if we're in a git repo
34
+ const isRepo = await git.checkIsRepo();
35
+ if (!isRepo) {
36
+ throw new Error('Not a git repository. Threadline requires a git repository.');
37
+ }
38
+ // Get all Bitbucket context
39
+ const diff = await getDiff(repoRoot);
40
+ const repoName = getRepoName();
41
+ const branchName = getBranchName();
42
+ const context = detectContext();
43
+ const commitSha = getCommitSha();
44
+ // Get commit author (from git log - Bitbucket doesn't provide this as env var)
45
+ const commitAuthor = await getCommitAuthor(repoRoot);
46
+ // Get commit message if we have a SHA
47
+ let commitMessage;
48
+ if (commitSha) {
49
+ const message = await (0, diff_1.getCommitMessage)(repoRoot, commitSha);
50
+ if (message) {
51
+ commitMessage = message;
52
+ }
53
+ }
54
+ return {
55
+ diff,
56
+ repoName,
57
+ branchName,
58
+ commitSha,
59
+ commitMessage,
60
+ commitAuthor,
61
+ prTitle: undefined, // Bitbucket doesn't expose PR title as env var
62
+ context
63
+ };
64
+ }
65
+ /**
66
+ * Get diff for Bitbucket Pipelines environment
67
+ *
68
+ * Bitbucket Pipelines with depth: full has full git history available,
69
+ * including origin/main. Unlike GitLab, no fetch is needed.
70
+ *
71
+ * Diff Strategy:
72
+ *
73
+ * | Scenario | Target Branch Known? | Diff Command |
74
+ * |---------------------------|---------------------------------------------|-------------------------------------------|
75
+ * | PR | ✅ Yes - BITBUCKET_PR_DESTINATION_BRANCH | origin/${destination}...HEAD |
76
+ * | Feature branch (no PR) | ❌ No - detect main/master | origin/main...HEAD or origin/master...HEAD|
77
+ * | Push to default branch | N/A | HEAD~1...HEAD |
78
+ *
79
+ * Key point: For PRs, Bitbucket provides BITBUCKET_PR_DESTINATION_BRANCH - this is the
80
+ * most relevant comparison point because it's where the code will be merged.
81
+ *
82
+ * For non-PR feature branches, Bitbucket does NOT provide a default branch env var
83
+ * (unlike GitLab's CI_DEFAULT_BRANCH), so we detect by checking if origin/main or
84
+ * origin/master exists.
85
+ */
86
+ async function getDiff(repoRoot) {
87
+ const git = (0, simple_git_1.default)(repoRoot);
88
+ const branchName = process.env.BITBUCKET_BRANCH;
89
+ const prId = process.env.BITBUCKET_PR_ID;
90
+ const prDestinationBranch = process.env.BITBUCKET_PR_DESTINATION_BRANCH;
91
+ // Scenario 1: PR context - use the target branch from env var
92
+ if (prId) {
93
+ if (!prDestinationBranch) {
94
+ throw new Error('Bitbucket PR context detected but BITBUCKET_PR_DESTINATION_BRANCH is not set. ' +
95
+ 'This should be automatically provided by Bitbucket Pipelines.');
96
+ }
97
+ console.log(` [Bitbucket] PR #${prId}, using origin/${prDestinationBranch}...HEAD`);
98
+ const diff = await git.diff([`origin/${prDestinationBranch}...HEAD`, '-U200']);
99
+ const diffSummary = await git.diffSummary([`origin/${prDestinationBranch}...HEAD`]);
100
+ const changedFiles = diffSummary.files.map(f => f.file);
101
+ return { diff: diff || '', changedFiles };
102
+ }
103
+ // Scenario 2: Non-PR push
104
+ if (!branchName) {
105
+ throw new Error('Bitbucket Pipelines: BITBUCKET_BRANCH environment variable is not set. ' +
106
+ 'This should be automatically provided by Bitbucket Pipelines.');
107
+ }
108
+ // Detect the default branch (Bitbucket doesn't provide this as an env var)
109
+ const defaultBranch = await detectDefaultBranch(git);
110
+ // If we're on the default branch, just show the last commit
111
+ if (branchName === defaultBranch) {
112
+ console.log(` [Bitbucket] Push to ${defaultBranch}, using HEAD~1...HEAD`);
113
+ const diff = await git.diff(['HEAD~1...HEAD', '-U200']);
114
+ const diffSummary = await git.diffSummary(['HEAD~1...HEAD']);
115
+ const changedFiles = diffSummary.files.map(f => f.file);
116
+ return { diff: diff || '', changedFiles };
117
+ }
118
+ // Feature branch: compare against default branch
119
+ // This shows all changes the branch introduces, correctly excluding
120
+ // any commits merged in from the default branch
121
+ console.log(` [Bitbucket] Feature branch "${branchName}", using origin/${defaultBranch}...HEAD`);
122
+ const diff = await git.diff([`origin/${defaultBranch}...HEAD`, '-U200']);
123
+ const diffSummary = await git.diffSummary([`origin/${defaultBranch}...HEAD`]);
124
+ const changedFiles = diffSummary.files.map(f => f.file);
125
+ return { diff: diff || '', changedFiles };
126
+ }
127
+ /**
128
+ * Detect the default branch for Bitbucket Pipelines.
129
+ *
130
+ * Bitbucket does NOT provide a default branch env var (unlike GitLab's CI_DEFAULT_BRANCH
131
+ * or GitHub's repository.default_branch in the event JSON).
132
+ *
133
+ * We try 'main' first (most common), then 'master' as fallback.
134
+ * This covers the vast majority of repositories.
135
+ *
136
+ * ---
137
+ * Design Decision: We compare against main instead of just checking the last commit
138
+ *
139
+ * Threadlines assumes that feature branches are intended to eventually merge to the
140
+ * default branch. Comparing against main shows ALL changes the branch introduces,
141
+ * which is what you want to review before merging.
142
+ *
143
+ * Per-commit checking happens during local development.
144
+ * ---
145
+ */
146
+ async function detectDefaultBranch(git) {
147
+ // Try 'main' first (modern default)
148
+ try {
149
+ await git.revparse(['--verify', 'origin/main']);
150
+ return 'main';
151
+ }
152
+ catch {
153
+ // origin/main doesn't exist, try master
154
+ }
155
+ // Try 'master' (legacy default)
156
+ try {
157
+ await git.revparse(['--verify', 'origin/master']);
158
+ return 'master';
159
+ }
160
+ catch {
161
+ // origin/master doesn't exist either
162
+ }
163
+ throw new Error('Bitbucket Pipelines: Cannot determine default branch. ' +
164
+ 'Neither origin/main nor origin/master found. ' +
165
+ 'For repositories with a different default branch, create a PR to trigger branch comparison.');
166
+ }
167
+ /**
168
+ * Gets repository name for Bitbucket Pipelines
169
+ *
170
+ * Uses BITBUCKET_REPO_FULL_NAME to construct the repo URL.
171
+ * Example: ngrootscholten/threadline -> https://bitbucket.org/ngrootscholten/threadline.git
172
+ */
173
+ function getRepoName() {
174
+ const repoFullName = process.env.BITBUCKET_REPO_FULL_NAME;
175
+ if (!repoFullName) {
176
+ throw new Error('Bitbucket Pipelines: BITBUCKET_REPO_FULL_NAME environment variable is not set. ' +
177
+ 'This should be automatically provided by Bitbucket Pipelines.');
178
+ }
179
+ return `https://bitbucket.org/${repoFullName}.git`;
180
+ }
181
+ /**
182
+ * Gets branch name for Bitbucket Pipelines
183
+ */
184
+ function getBranchName() {
185
+ const branchName = process.env.BITBUCKET_BRANCH;
186
+ if (!branchName) {
187
+ throw new Error('Bitbucket Pipelines: BITBUCKET_BRANCH environment variable is not set. ' +
188
+ 'This should be automatically provided by Bitbucket Pipelines.');
189
+ }
190
+ return branchName;
191
+ }
192
+ /**
193
+ * Detects Bitbucket context (PR, branch, or commit)
194
+ */
195
+ function detectContext() {
196
+ // PR context
197
+ const prId = process.env.BITBUCKET_PR_ID;
198
+ const prDestinationBranch = process.env.BITBUCKET_PR_DESTINATION_BRANCH;
199
+ const sourceBranch = process.env.BITBUCKET_BRANCH;
200
+ if (prId && prDestinationBranch && sourceBranch) {
201
+ return {
202
+ type: 'pr',
203
+ prNumber: prId,
204
+ sourceBranch,
205
+ targetBranch: prDestinationBranch
206
+ };
207
+ }
208
+ // Branch context
209
+ if (process.env.BITBUCKET_BRANCH) {
210
+ return {
211
+ type: 'branch',
212
+ branchName: process.env.BITBUCKET_BRANCH
213
+ };
214
+ }
215
+ // Commit context
216
+ if (process.env.BITBUCKET_COMMIT) {
217
+ return {
218
+ type: 'commit',
219
+ commitSha: process.env.BITBUCKET_COMMIT
220
+ };
221
+ }
222
+ // Fallback to local (shouldn't happen in Bitbucket Pipelines)
223
+ return { type: 'local' };
224
+ }
225
+ /**
226
+ * Gets commit SHA from Bitbucket environment
227
+ */
228
+ function getCommitSha() {
229
+ return process.env.BITBUCKET_COMMIT;
230
+ }
231
+ /**
232
+ * Gets commit author for Bitbucket Pipelines
233
+ *
234
+ * Bitbucket doesn't provide commit author as an environment variable,
235
+ * so we use git log to get it.
236
+ *
237
+ * This approach is verified by our test script (test-bitbucket-context.ts)
238
+ * which successfully retrieves commit author in all scenarios:
239
+ * - Direct commit to main
240
+ * - Feature branch push
241
+ * - PR pipeline
242
+ * - Merge commit
243
+ */
244
+ async function getCommitAuthor(repoRoot) {
245
+ // Use raw git commands - this is exactly what the test script uses and we know it works
246
+ try {
247
+ const name = (0, child_process_1.execSync)('git log -1 --format=%an', { encoding: 'utf-8', cwd: repoRoot }).trim();
248
+ const email = (0, child_process_1.execSync)('git log -1 --format=%ae', { encoding: 'utf-8', cwd: repoRoot }).trim();
249
+ if (!name || !email) {
250
+ throw new Error('git log returned empty name or email');
251
+ }
252
+ return { name, email };
253
+ }
254
+ catch (error) {
255
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
256
+ throw new Error(`Bitbucket Pipelines: Failed to get commit author from git log. ` +
257
+ `Error: ${errorMessage}`);
258
+ }
259
+ }
package/dist/git/repo.js CHANGED
@@ -213,7 +213,7 @@ async function getGitLabBranchName(_repoRoot) {
213
213
  * Uses GITHUB_EVENT_PATH JSON (repository.default_branch) - the most authoritative source
214
214
  * provided directly by GitHub Actions.
215
215
  *
216
- * This function is ONLY called from GitHub Actions context (getGitHubDiff),
216
+ * This function is ONLY called from GitHub Actions context (github.ts),
217
217
  * so GITHUB_EVENT_PATH should always be available. If it's not, we fail with a clear error.
218
218
  *
219
219
  * Returns the branch name (e.g., "main", "master") without the "origin/" prefix.
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ /**
3
+ * Git-related type definitions
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -25,6 +25,8 @@ function detectContext(environment) {
25
25
  return detectGitHubContext();
26
26
  case 'gitlab':
27
27
  return detectGitLabContext();
28
+ case 'bitbucket':
29
+ return detectBitbucketContext();
28
30
  case 'vercel':
29
31
  return detectVercelContext();
30
32
  case 'local':
@@ -146,3 +148,43 @@ function detectVercelContext() {
146
148
  // Fallback to local
147
149
  return { type: 'local' };
148
150
  }
151
+ /**
152
+ * Bitbucket Pipelines context detection
153
+ *
154
+ * Environment Variables (all tested 2026-01-18):
155
+ * - Branch: BITBUCKET_BRANCH
156
+ * - Commit: BITBUCKET_COMMIT
157
+ * - PR: BITBUCKET_PR_ID, BITBUCKET_PR_DESTINATION_BRANCH
158
+ *
159
+ * Note: Bitbucket does not provide PR title as an environment variable.
160
+ */
161
+ function detectBitbucketContext() {
162
+ // PR context
163
+ const prId = process.env.BITBUCKET_PR_ID;
164
+ const prDestinationBranch = process.env.BITBUCKET_PR_DESTINATION_BRANCH;
165
+ const sourceBranch = process.env.BITBUCKET_BRANCH;
166
+ if (prId && prDestinationBranch && sourceBranch) {
167
+ return {
168
+ type: 'pr',
169
+ prNumber: prId,
170
+ sourceBranch,
171
+ targetBranch: prDestinationBranch
172
+ };
173
+ }
174
+ // Branch context
175
+ if (process.env.BITBUCKET_BRANCH) {
176
+ return {
177
+ type: 'branch',
178
+ branchName: process.env.BITBUCKET_BRANCH
179
+ };
180
+ }
181
+ // Commit context
182
+ if (process.env.BITBUCKET_COMMIT) {
183
+ return {
184
+ type: 'commit',
185
+ commitSha: process.env.BITBUCKET_COMMIT
186
+ };
187
+ }
188
+ // Fallback to local
189
+ return { type: 'local' };
190
+ }
@@ -15,7 +15,8 @@ exports.isCIEnvironment = isCIEnvironment;
15
15
  * 1. Vercel: VERCEL=1
16
16
  * 2. GitHub Actions: GITHUB_ACTIONS=1
17
17
  * 3. GitLab CI: GITLAB_CI=1 or (CI=1 + CI_COMMIT_SHA)
18
- * 4. Local: None of the above
18
+ * 4. Bitbucket Pipelines: BITBUCKET_BUILD_NUMBER exists
19
+ * 5. Local: None of the above
19
20
  */
20
21
  function detectEnvironment() {
21
22
  if (process.env.VERCEL)
@@ -24,6 +25,8 @@ function detectEnvironment() {
24
25
  return 'github';
25
26
  if (process.env.GITLAB_CI || (process.env.CI && process.env.CI_COMMIT_SHA))
26
27
  return 'gitlab';
28
+ if (process.env.BITBUCKET_BUILD_NUMBER)
29
+ return 'bitbucket';
27
30
  return 'local';
28
31
  }
29
32
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadlines",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Threadlines CLI - AI-powered linter based on your natural language documentation",
5
5
  "main": "dist/index.js",
6
6
  "bin": {