threadlines 0.1.17 → 0.1.18

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.
@@ -38,38 +38,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.checkCommand = checkCommand;
40
40
  const experts_1 = require("../validators/experts");
41
- const diff_1 = require("../git/diff");
42
41
  const file_1 = require("../git/file");
43
42
  const repo_1 = require("../git/repo");
44
- const ci_detection_1 = require("../utils/ci-detection");
45
43
  const client_1 = require("../api/client");
46
44
  const config_1 = require("../utils/config");
45
+ const environment_1 = require("../utils/environment");
46
+ const context_1 = require("../utils/context");
47
+ const metadata_1 = require("../utils/metadata");
48
+ const git_diff_executor_1 = require("../utils/git-diff-executor");
47
49
  const fs = __importStar(require("fs"));
48
50
  const path = __importStar(require("path"));
49
51
  const chalk_1 = __importDefault(require("chalk"));
50
- /**
51
- * Detect the environment where the check is being run.
52
- *
53
- * CI Environment Detection:
54
- * - Vercel: VERCEL=1
55
- * - GitHub Actions: GITHUB_ACTIONS=1
56
- * - GitLab CI: GITLAB_CI=1 or (CI=1 + CI_COMMIT_SHA)
57
- * - Local: None of the above
58
- *
59
- * Available CI Environment Variables:
60
- * - GitHub Actions: GITHUB_REPOSITORY, GITHUB_REF_NAME, GITHUB_SHA, GITHUB_BASE_REF, GITHUB_HEAD_REF
61
- * - Vercel: VERCEL_GIT_REPO_OWNER, VERCEL_GIT_REPO_SLUG, VERCEL_GIT_COMMIT_REF, VERCEL_GIT_COMMIT_SHA
62
- * - GitLab CI: CI_COMMIT_REF_NAME, CI_COMMIT_SHA, CI_MERGE_REQUEST_IID, CI_MERGE_REQUEST_TITLE
63
- */
64
- function detectEnvironment() {
65
- if (process.env.VERCEL)
66
- return 'vercel';
67
- if (process.env.GITHUB_ACTIONS)
68
- return 'github';
69
- if (process.env.GITLAB_CI || (process.env.CI && process.env.CI_COMMIT_SHA))
70
- return 'gitlab';
71
- return 'local';
72
- }
73
52
  async function checkCommand(options) {
74
53
  const repoRoot = process.cwd();
75
54
  console.log(chalk_1.default.blue('🔍 Threadline: Checking code against your threadlines...\n'));
@@ -109,12 +88,10 @@ async function checkCommand(options) {
109
88
  console.log(chalk_1.default.gray(' Run `npx threadlines init` to create your first threadline.'));
110
89
  process.exit(0);
111
90
  }
112
- // 2. Determine review target and get git diff
91
+ // 2. Detect environment and context
92
+ const environment = (0, environment_1.detectEnvironment)();
93
+ let context;
113
94
  let gitDiff;
114
- let reviewContext = { type: 'local' };
115
- let commitSha = undefined;
116
- let commitMessage = undefined;
117
- let prTitle = undefined;
118
95
  // Validate mutually exclusive flags
119
96
  const explicitFlags = [options.branch, options.commit, options.file, options.folder, options.files].filter(Boolean);
120
97
  if (explicitFlags.length > 1) {
@@ -126,91 +103,37 @@ async function checkCommand(options) {
126
103
  if (options.file) {
127
104
  console.log(chalk_1.default.gray(`📝 Reading file: ${options.file}...`));
128
105
  gitDiff = await (0, file_1.getFileContent)(repoRoot, options.file);
129
- reviewContext = { type: 'file', value: options.file };
106
+ context = { type: 'local' }; // File context doesn't need git context
130
107
  }
131
108
  else if (options.folder) {
132
109
  console.log(chalk_1.default.gray(`📝 Reading folder: ${options.folder}...`));
133
110
  gitDiff = await (0, file_1.getFolderContent)(repoRoot, options.folder);
134
- reviewContext = { type: 'folder', value: options.folder };
111
+ context = { type: 'local' };
135
112
  }
136
113
  else if (options.files && options.files.length > 0) {
137
114
  console.log(chalk_1.default.gray(`📝 Reading ${options.files.length} file(s)...`));
138
115
  gitDiff = await (0, file_1.getMultipleFilesContent)(repoRoot, options.files);
139
- reviewContext = { type: 'files', value: options.files.join(', ') };
116
+ context = { type: 'local' };
140
117
  }
141
118
  else if (options.branch) {
142
119
  console.log(chalk_1.default.gray(`📝 Collecting git changes for branch: ${options.branch}...`));
143
- gitDiff = await (0, diff_1.getBranchDiff)(repoRoot, options.branch);
144
- reviewContext = { type: 'branch', value: options.branch };
120
+ context = { type: 'branch', branchName: options.branch };
121
+ gitDiff = await (0, git_diff_executor_1.getDiffForContext)(context, repoRoot, environment);
145
122
  }
146
123
  else if (options.commit) {
147
124
  console.log(chalk_1.default.gray(`📝 Collecting git changes for commit: ${options.commit}...`));
148
- gitDiff = await (0, diff_1.getCommitDiff)(repoRoot, options.commit);
149
- reviewContext = { type: 'commit', value: options.commit };
150
- commitSha = options.commit;
151
- // Fetch commit message (reliable when we have SHA)
152
- const message = await (0, diff_1.getCommitMessage)(repoRoot, options.commit);
153
- if (message) {
154
- commitMessage = message;
155
- }
125
+ context = { type: 'commit', commitSha: options.commit };
126
+ gitDiff = await (0, git_diff_executor_1.getDiffForContext)(context, repoRoot, environment);
156
127
  }
157
128
  else {
158
- // Auto-detect CI environment or use local changes
159
- const autoTarget = (0, ci_detection_1.getAutoReviewTarget)();
160
- if (autoTarget) {
161
- if (autoTarget.type === 'pr' || autoTarget.type === 'mr') {
162
- // PR/MR: use source and target branches
163
- console.log(chalk_1.default.gray(`📝 Collecting git changes for ${autoTarget.type.toUpperCase()}: ${autoTarget.value}...`));
164
- gitDiff = await (0, diff_1.getPRMRDiff)(repoRoot, autoTarget.sourceBranch, autoTarget.targetBranch);
165
- reviewContext = { type: autoTarget.type, value: autoTarget.value };
166
- // Use PR title from GitLab CI (reliable env var)
167
- if (autoTarget.prTitle) {
168
- prTitle = autoTarget.prTitle;
169
- }
170
- }
171
- else if (autoTarget.type === 'branch') {
172
- // Branch: use branch vs base
173
- console.log(chalk_1.default.gray(`📝 Collecting git changes for branch: ${autoTarget.value}...`));
174
- gitDiff = await (0, diff_1.getBranchDiff)(repoRoot, autoTarget.value);
175
- reviewContext = { type: 'branch', value: autoTarget.value };
176
- // Capture commit SHA from CI env vars if available
177
- if (process.env.GITHUB_SHA) {
178
- commitSha = process.env.GITHUB_SHA;
179
- const message = await (0, diff_1.getCommitMessage)(repoRoot, process.env.GITHUB_SHA);
180
- if (message)
181
- commitMessage = message;
182
- }
183
- else if (process.env.VERCEL_GIT_COMMIT_SHA) {
184
- commitSha = process.env.VERCEL_GIT_COMMIT_SHA;
185
- const message = await (0, diff_1.getCommitMessage)(repoRoot, process.env.VERCEL_GIT_COMMIT_SHA);
186
- if (message)
187
- commitMessage = message;
188
- }
189
- }
190
- else if (autoTarget.type === 'commit') {
191
- // Commit: use single commit
192
- console.log(chalk_1.default.gray(`📝 Collecting git changes for commit: ${autoTarget.value}...`));
193
- gitDiff = await (0, diff_1.getCommitDiff)(repoRoot, autoTarget.value);
194
- reviewContext = { type: 'commit', value: autoTarget.value };
195
- commitSha = autoTarget.value;
196
- // Fetch commit message (reliable when we have SHA)
197
- const message = await (0, diff_1.getCommitMessage)(repoRoot, autoTarget.value);
198
- if (message) {
199
- commitMessage = message;
200
- }
201
- }
202
- else {
203
- // Fallback: local development
204
- console.log(chalk_1.default.gray('📝 Collecting git changes...'));
205
- gitDiff = await (0, diff_1.getGitDiff)(repoRoot);
206
- }
207
- }
208
- else {
209
- // Local development: use staged/unstaged changes
210
- console.log(chalk_1.default.gray('📝 Collecting git changes...'));
211
- gitDiff = await (0, diff_1.getGitDiff)(repoRoot);
212
- }
129
+ // Auto-detect context based on environment
130
+ context = (0, context_1.detectContext)(environment);
131
+ const contextDesc = (0, git_diff_executor_1.getContextDescription)(context);
132
+ console.log(chalk_1.default.gray(`📝 Collecting git changes for ${contextDesc}...`));
133
+ gitDiff = await (0, git_diff_executor_1.getDiffForContext)(context, repoRoot, environment);
213
134
  }
135
+ // 3. Collect metadata (commit SHA, commit message, PR title)
136
+ const metadata = await (0, metadata_1.collectMetadata)(context, environment, repoRoot);
214
137
  if (gitDiff.changedFiles.length === 0) {
215
138
  console.log(chalk_1.default.yellow('⚠️ No changes detected. Make some code changes and try again.'));
216
139
  process.exit(0);
@@ -254,9 +177,7 @@ async function checkCommand(options) {
254
177
  const apiUrl = options.apiUrl ||
255
178
  process.env.THREADLINE_API_URL ||
256
179
  'https://devthreadline.com';
257
- // 6. Detect environment
258
- const environment = detectEnvironment();
259
- // 7. Call review API
180
+ // 6. Call review API
260
181
  console.log(chalk_1.default.gray('🤖 Running threadline checks...'));
261
182
  const client = new client_1.ReviewAPIClient(apiUrl);
262
183
  const response = await client.review({
@@ -267,9 +188,9 @@ async function checkCommand(options) {
267
188
  account,
268
189
  repoName: repoName || undefined,
269
190
  branchName: branchName || undefined,
270
- commitSha: commitSha,
271
- commitMessage: commitMessage,
272
- prTitle: prTitle,
191
+ commitSha: metadata.commitSha,
192
+ commitMessage: metadata.commitMessage,
193
+ prTitle: metadata.prTitle,
273
194
  environment: environment
274
195
  });
275
196
  // 7. Display results (with filtering if --full not specified)
package/dist/git/diff.js CHANGED
@@ -71,20 +71,19 @@ async function getBranchDiff(repoRoot, branchName, baseBranch) {
71
71
  // For main/master branch, compare against previous commit (HEAD~1)
72
72
  // This checks what changed in the most recent commit
73
73
  try {
74
- const previousCommit = await git.revparse(['HEAD~1']).catch(() => null);
75
- if (previousCommit) {
76
- // Use commit-based diff instead
77
- const diff = await git.diff([`${previousCommit}..HEAD`, '-U200']);
78
- const diffSummary = await git.diffSummary([`${previousCommit}..HEAD`]);
79
- const changedFiles = diffSummary.files.map(f => f.file);
80
- return {
81
- diff: diff || '',
82
- changedFiles
83
- };
84
- }
74
+ const previousCommit = await git.revparse(['HEAD~1']);
75
+ // Use commit-based diff instead
76
+ const diff = await git.diff([`${previousCommit}..HEAD`, '-U200']);
77
+ const diffSummary = await git.diffSummary([`${previousCommit}..HEAD`]);
78
+ const changedFiles = diffSummary.files.map(f => f.file);
79
+ return {
80
+ diff: diff || '',
81
+ changedFiles
82
+ };
85
83
  }
86
- catch {
84
+ catch (error) {
87
85
  // If no previous commit, return empty (first commit)
86
+ console.log(`[DEBUG] No previous commit found (first commit or error): ${error.message || 'HEAD~1 does not exist'}`);
88
87
  return {
89
88
  diff: '',
90
89
  changedFiles: []
@@ -96,51 +95,103 @@ async function getBranchDiff(repoRoot, branchName, baseBranch) {
96
95
  }
97
96
  // Helper function to detect base branch
98
97
  // Returns the branch name to use in git commands (may be local or remote)
98
+ // In CI environments, prioritizes remote refs since local branches often don't exist
99
99
  async function detectBaseBranch(git, branchName) {
100
- // Try upstream tracking branch
101
- const upstream = await git.revparse(['--abbrev-ref', '--symbolic-full-name', `${branchName}@{u}`]).catch(() => null);
102
- if (upstream) {
103
- // Extract base from upstream (e.g., "origin/main" -> check if local exists, else use "origin/main")
100
+ const isCI = !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.VERCEL || process.env.GITLAB_CI);
101
+ // Strategy 1: Try upstream tracking branch (most reliable if set)
102
+ try {
103
+ const upstream = await git.revparse(['--abbrev-ref', '--symbolic-full-name', `${branchName}@{u}`]);
104
104
  const upstreamBranch = upstream.replace(/^origin\//, '');
105
105
  // Don't use the branch itself as its base
106
106
  if (upstreamBranch !== branchName) {
107
- // Check if local branch exists, otherwise use remote
108
- const localExists = await git.revparse([upstreamBranch]).catch(() => null);
109
- return localExists ? upstreamBranch : upstream;
107
+ // In CI, prefer remote refs since local branches often don't exist
108
+ if (isCI) {
109
+ console.log(`[DEBUG] CI environment detected, using upstream tracking branch (remote): ${upstream}`);
110
+ return upstream;
111
+ }
112
+ // In local dev, check if local branch exists
113
+ try {
114
+ await git.revparse([upstreamBranch]);
115
+ console.log(`[DEBUG] Using upstream tracking branch (local): ${upstreamBranch}`);
116
+ return upstreamBranch;
117
+ }
118
+ catch {
119
+ console.log(`[DEBUG] Upstream tracking branch exists but local branch '${upstreamBranch}' not found, using remote: ${upstream}`);
120
+ return upstream;
121
+ }
122
+ }
123
+ else {
124
+ console.log(`[DEBUG] Upstream tracking branch '${upstreamBranch}' is the same as current branch, skipping`);
110
125
  }
111
126
  }
112
- // Try default branch
127
+ catch (error) {
128
+ console.log(`[DEBUG] Upstream tracking branch not set for '${branchName}': ${error.message || 'no upstream configured'}`);
129
+ }
130
+ // Strategy 2: Try default branch from origin/HEAD (reliable if configured)
113
131
  try {
114
132
  const defaultBranch = await git.revparse(['--abbrev-ref', 'refs/remotes/origin/HEAD']);
115
133
  const defaultBranchName = defaultBranch.replace(/^origin\//, '');
116
134
  // Don't use the branch itself as its base
117
135
  if (defaultBranchName !== branchName) {
118
- // Check if local branch exists, otherwise use remote
119
- const localExists = await git.revparse([defaultBranchName]).catch(() => null);
120
- return localExists ? defaultBranchName : defaultBranch;
136
+ // In CI, prefer remote refs
137
+ if (isCI) {
138
+ console.log(`[DEBUG] CI environment detected, using default branch (remote): ${defaultBranch}`);
139
+ return defaultBranch;
140
+ }
141
+ // In local dev, check if local branch exists
142
+ try {
143
+ await git.revparse([defaultBranchName]);
144
+ console.log(`[DEBUG] Using default branch (local): ${defaultBranchName}`);
145
+ return defaultBranchName;
146
+ }
147
+ catch {
148
+ console.log(`[DEBUG] Default branch exists but local branch '${defaultBranchName}' not found, using remote: ${defaultBranch}`);
149
+ return defaultBranch;
150
+ }
151
+ }
152
+ else {
153
+ console.log(`[DEBUG] Default branch '${defaultBranchName}' is the same as current branch, skipping`);
121
154
  }
122
155
  }
123
- catch {
124
- // Continue to fallback
156
+ catch (error) {
157
+ console.log(`[DEBUG] Default branch (refs/remotes/origin/HEAD) not configured: ${error.message || 'not found'}`);
125
158
  }
126
- // Fallback to common names (excluding the branch itself)
159
+ // Strategy 3: Try common branch names by checking remote refs first (most reliable fallback)
160
+ // This works reliably in CI with fetch-depth: 0, and also works locally
127
161
  const commonBases = ['main', 'master', 'develop'];
128
162
  for (const candidate of commonBases) {
129
163
  if (candidate.toLowerCase() === branchName.toLowerCase()) {
130
164
  continue; // Skip if it's the same branch
131
165
  }
132
166
  try {
133
- // Check if remote exists
167
+ // Always check remote ref first (this is reliable in CI with fetch-depth: 0)
134
168
  await git.revparse([`origin/${candidate}`]);
135
- // Check if local exists, otherwise use remote
136
- const localExists = await git.revparse([candidate]).catch(() => null);
137
- return localExists ? candidate : `origin/${candidate}`;
169
+ // In CI, prefer remote refs since local branches often don't exist
170
+ if (isCI) {
171
+ console.log(`[DEBUG] CI environment detected, using common branch name (remote): origin/${candidate}`);
172
+ return `origin/${candidate}`;
173
+ }
174
+ // In local dev, check if local branch exists
175
+ try {
176
+ await git.revparse([candidate]);
177
+ console.log(`[DEBUG] Using common branch name (local): ${candidate}`);
178
+ return candidate;
179
+ }
180
+ catch {
181
+ console.log(`[DEBUG] Common branch '${candidate}' exists remotely but not locally, using remote: origin/${candidate}`);
182
+ return `origin/${candidate}`;
183
+ }
138
184
  }
139
- catch {
140
- // Try next
185
+ catch (error) {
186
+ console.log(`[DEBUG] Remote branch 'origin/${candidate}' not found: ${error.message || 'does not exist'}`);
187
+ // Continue to next candidate
141
188
  }
142
189
  }
143
- throw new Error(`Could not determine base branch for '${branchName}'. Please specify with --base flag or set upstream tracking.`);
190
+ // All strategies failed - provide clear error with context
191
+ throw new Error(`Could not determine base branch for '${branchName}'. ` +
192
+ `Tried: upstream tracking, default branch (origin/HEAD), and common names (main, master, develop). ` +
193
+ `Please specify base branch with --base flag or configure upstream tracking with: ` +
194
+ `git branch --set-upstream-to=origin/main ${branchName}`);
144
195
  }
145
196
  // Get diff between base and branch (cumulative diff of all commits)
146
197
  // Format: git diff base...branch (three-dot notation finds common ancestor)
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ /**
3
+ * Review Context Detection
4
+ *
5
+ * Determines what type of code review context we're in:
6
+ * - PR/MR: Multiple commits comparing two branches
7
+ * - Branch: All commits on a branch vs base branch
8
+ * - Commit: Single commit changes
9
+ * - Local: Staged/unstaged changes in working directory
10
+ *
11
+ * Context detection is environment-specific - each CI platform
12
+ * provides different environment variables.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.detectContext = detectContext;
16
+ /**
17
+ * Detects the review context based on the environment.
18
+ *
19
+ * Each environment has different environment variables available,
20
+ * so detection logic is environment-specific.
21
+ */
22
+ function detectContext(environment) {
23
+ switch (environment) {
24
+ case 'github':
25
+ return detectGitHubContext();
26
+ case 'gitlab':
27
+ return detectGitLabContext();
28
+ case 'vercel':
29
+ return detectVercelContext();
30
+ case 'local':
31
+ return { type: 'local' };
32
+ case 'azure-devops':
33
+ // Future: return detectAzureDevOpsContext();
34
+ return { type: 'local' };
35
+ default:
36
+ return { type: 'local' };
37
+ }
38
+ }
39
+ /**
40
+ * GitHub Actions context detection
41
+ *
42
+ * Environment Variables:
43
+ * - PR: GITHUB_EVENT_NAME='pull_request', GITHUB_BASE_REF, GITHUB_HEAD_REF, GITHUB_EVENT_NUMBER
44
+ * - Branch: GITHUB_REF_NAME
45
+ * - Commit: GITHUB_SHA
46
+ */
47
+ function detectGitHubContext() {
48
+ // 1. Check for PR context
49
+ if (process.env.GITHUB_EVENT_NAME === 'pull_request') {
50
+ const targetBranch = process.env.GITHUB_BASE_REF;
51
+ const sourceBranch = process.env.GITHUB_HEAD_REF;
52
+ const prNumber = process.env.GITHUB_EVENT_PULL_REQUEST_NUMBER || process.env.GITHUB_EVENT_NUMBER;
53
+ if (targetBranch && sourceBranch && prNumber) {
54
+ return {
55
+ type: 'pr',
56
+ prNumber,
57
+ sourceBranch,
58
+ targetBranch
59
+ };
60
+ }
61
+ }
62
+ // 2. Check for branch context
63
+ if (process.env.GITHUB_REF_NAME) {
64
+ return {
65
+ type: 'branch',
66
+ branchName: process.env.GITHUB_REF_NAME
67
+ };
68
+ }
69
+ // 3. Check for commit context
70
+ if (process.env.GITHUB_SHA) {
71
+ return {
72
+ type: 'commit',
73
+ commitSha: process.env.GITHUB_SHA
74
+ };
75
+ }
76
+ // 4. Fallback to local
77
+ return { type: 'local' };
78
+ }
79
+ /**
80
+ * GitLab CI context detection
81
+ *
82
+ * Environment Variables:
83
+ * - MR: CI_MERGE_REQUEST_IID, CI_MERGE_REQUEST_TARGET_BRANCH_NAME, CI_MERGE_REQUEST_SOURCE_BRANCH_NAME, CI_MERGE_REQUEST_TITLE
84
+ * - Branch: CI_COMMIT_REF_NAME
85
+ * - Commit: CI_COMMIT_SHA
86
+ */
87
+ function detectGitLabContext() {
88
+ // 1. Check for MR context
89
+ if (process.env.CI_MERGE_REQUEST_IID) {
90
+ const targetBranch = process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME;
91
+ const sourceBranch = process.env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME;
92
+ const mrNumber = process.env.CI_MERGE_REQUEST_IID;
93
+ const mrTitle = process.env.CI_MERGE_REQUEST_TITLE;
94
+ if (targetBranch && sourceBranch && mrNumber) {
95
+ return {
96
+ type: 'mr',
97
+ mrNumber,
98
+ sourceBranch,
99
+ targetBranch,
100
+ prTitle: mrTitle || undefined
101
+ };
102
+ }
103
+ }
104
+ // 2. Check for branch context
105
+ if (process.env.CI_COMMIT_REF_NAME) {
106
+ return {
107
+ type: 'branch',
108
+ branchName: process.env.CI_COMMIT_REF_NAME
109
+ };
110
+ }
111
+ // 3. Check for commit context
112
+ if (process.env.CI_COMMIT_SHA) {
113
+ return {
114
+ type: 'commit',
115
+ commitSha: process.env.CI_COMMIT_SHA
116
+ };
117
+ }
118
+ // 4. Fallback to local
119
+ return { type: 'local' };
120
+ }
121
+ /**
122
+ * Vercel context detection
123
+ *
124
+ * Environment Variables:
125
+ * - Branch: VERCEL_GIT_COMMIT_REF
126
+ * - Commit: VERCEL_GIT_COMMIT_SHA
127
+ */
128
+ function detectVercelContext() {
129
+ // 1. Check for branch context
130
+ if (process.env.VERCEL_GIT_COMMIT_REF) {
131
+ return {
132
+ type: 'branch',
133
+ branchName: process.env.VERCEL_GIT_COMMIT_REF
134
+ };
135
+ }
136
+ // 2. Check for commit context
137
+ if (process.env.VERCEL_GIT_COMMIT_SHA) {
138
+ return {
139
+ type: 'commit',
140
+ commitSha: process.env.VERCEL_GIT_COMMIT_SHA
141
+ };
142
+ }
143
+ // 3. Fallback to local
144
+ return { type: 'local' };
145
+ }
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ /**
3
+ * CI/CD Environment Detection
4
+ *
5
+ * Detects which CI/CD platform or local environment the code is running in.
6
+ * This is the first step in determining how to collect context and metadata.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.detectEnvironment = detectEnvironment;
10
+ exports.isCIEnvironment = isCIEnvironment;
11
+ /**
12
+ * Detects the current environment based on environment variables.
13
+ *
14
+ * Detection order:
15
+ * 1. Vercel: VERCEL=1
16
+ * 2. GitHub Actions: GITHUB_ACTIONS=1
17
+ * 3. GitLab CI: GITLAB_CI=1 or (CI=1 + CI_COMMIT_SHA)
18
+ * 4. Azure DevOps: TF_BUILD=1 (future)
19
+ * 5. Local: None of the above
20
+ */
21
+ function detectEnvironment() {
22
+ if (process.env.VERCEL)
23
+ return 'vercel';
24
+ if (process.env.GITHUB_ACTIONS)
25
+ return 'github';
26
+ if (process.env.GITLAB_CI || (process.env.CI && process.env.CI_COMMIT_SHA))
27
+ return 'gitlab';
28
+ // Future: if (process.env.TF_BUILD) return 'azure-devops';
29
+ return 'local';
30
+ }
31
+ /**
32
+ * Returns true if running in a CI/CD environment (not local)
33
+ */
34
+ function isCIEnvironment(env) {
35
+ return env !== 'local';
36
+ }
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ /**
3
+ * Git Diff Execution
4
+ *
5
+ * Executes the appropriate git diff function based on review context.
6
+ * This is environment-agnostic - the git diff functions themselves
7
+ * handle environment-specific details (like base branch detection).
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.getDiffForContext = getDiffForContext;
11
+ exports.getContextDescription = getContextDescription;
12
+ const diff_1 = require("../git/diff");
13
+ /**
14
+ * Executes the appropriate git diff function based on context.
15
+ *
16
+ * This function maps context types to their corresponding git diff functions.
17
+ * The actual git diff functions handle environment-specific details internally.
18
+ */
19
+ async function getDiffForContext(context, repoRoot, environment) {
20
+ switch (context.type) {
21
+ case 'pr':
22
+ return await (0, diff_1.getPRMRDiff)(repoRoot, context.sourceBranch, context.targetBranch);
23
+ case 'mr':
24
+ return await (0, diff_1.getPRMRDiff)(repoRoot, context.sourceBranch, context.targetBranch);
25
+ case 'branch':
26
+ return await (0, diff_1.getBranchDiff)(repoRoot, context.branchName);
27
+ case 'commit':
28
+ return await (0, diff_1.getCommitDiff)(repoRoot, context.commitSha);
29
+ case 'local':
30
+ return await (0, diff_1.getGitDiff)(repoRoot);
31
+ default:
32
+ // TypeScript exhaustiveness check - should never reach here
33
+ const _exhaustive = context;
34
+ throw new Error(`Unknown context type: ${_exhaustive}`);
35
+ }
36
+ }
37
+ /**
38
+ * Returns a human-readable description of the context for logging.
39
+ */
40
+ function getContextDescription(context) {
41
+ switch (context.type) {
42
+ case 'pr':
43
+ return `PR: ${context.prNumber}`;
44
+ case 'mr':
45
+ return `MR: ${context.mrNumber}`;
46
+ case 'branch':
47
+ return `branch: ${context.branchName}`;
48
+ case 'commit':
49
+ return `commit: ${context.commitSha.substring(0, 7)}`;
50
+ case 'local':
51
+ return 'local changes';
52
+ default:
53
+ const _exhaustive = context;
54
+ throw new Error(`Unknown context type: ${_exhaustive}`);
55
+ }
56
+ }
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ /**
3
+ * Metadata Collection
4
+ *
5
+ * Collects environment-specific metadata for review context:
6
+ * - Commit SHA (from env vars or git)
7
+ * - Commit message (from git)
8
+ * - PR/MR title (from env vars or API)
9
+ *
10
+ * Metadata collection is environment-specific because each CI platform
11
+ * provides different environment variables.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.collectMetadata = collectMetadata;
15
+ const diff_1 = require("../git/diff");
16
+ /**
17
+ * Collects metadata for the given context and environment.
18
+ *
19
+ * This function knows how to extract metadata from each environment's
20
+ * specific environment variables and git commands.
21
+ */
22
+ async function collectMetadata(context, environment, repoRoot) {
23
+ const metadata = {};
24
+ // Collect commit SHA (environment-specific)
25
+ metadata.commitSha = getCommitSha(context, environment);
26
+ // Collect commit message (if we have a commit SHA)
27
+ if (metadata.commitSha) {
28
+ const message = await (0, diff_1.getCommitMessage)(repoRoot, metadata.commitSha);
29
+ if (message) {
30
+ metadata.commitMessage = message;
31
+ }
32
+ }
33
+ // Collect PR/MR title (environment-specific)
34
+ metadata.prTitle = getPRTitle(context, environment);
35
+ return metadata;
36
+ }
37
+ /**
38
+ * Extracts commit SHA from context and environment.
39
+ */
40
+ function getCommitSha(context, environment) {
41
+ // If context already has commit SHA, use it
42
+ if (context.type === 'commit') {
43
+ return context.commitSha;
44
+ }
45
+ // For branch contexts, try to get commit SHA from environment variables
46
+ if (context.type === 'branch') {
47
+ switch (environment) {
48
+ case 'github':
49
+ return process.env.GITHUB_SHA;
50
+ case 'gitlab':
51
+ return process.env.CI_COMMIT_SHA;
52
+ case 'vercel':
53
+ return process.env.VERCEL_GIT_COMMIT_SHA;
54
+ default:
55
+ return undefined;
56
+ }
57
+ }
58
+ // For PR/MR contexts, commit SHA might be available in env vars
59
+ if (context.type === 'pr' || context.type === 'mr') {
60
+ switch (environment) {
61
+ case 'github':
62
+ // For PRs, GITHUB_SHA is a merge commit, might want GITHUB_HEAD_SHA instead
63
+ return process.env.GITHUB_HEAD_SHA || process.env.GITHUB_SHA;
64
+ case 'gitlab':
65
+ return process.env.CI_COMMIT_SHA;
66
+ default:
67
+ return undefined;
68
+ }
69
+ }
70
+ return undefined;
71
+ }
72
+ /**
73
+ * Extracts PR/MR title from context and environment.
74
+ *
75
+ * Note: GitHub Actions doesn't provide PR title as an env var by default.
76
+ * It would need to be passed from the workflow YAML or fetched via API.
77
+ */
78
+ function getPRTitle(context, environment) {
79
+ // Only PR/MR contexts have titles
80
+ if (context.type !== 'pr' && context.type !== 'mr') {
81
+ return undefined;
82
+ }
83
+ // GitLab CI provides MR title as env var
84
+ if (context.type === 'mr' && environment === 'gitlab') {
85
+ return context.prTitle;
86
+ }
87
+ // GitHub Actions doesn't provide PR title as env var
88
+ // Would need to be passed from workflow: PR_TITLE: ${{ github.event.pull_request.title }}
89
+ // or fetched via GitHub API
90
+ if (context.type === 'pr' && environment === 'github') {
91
+ return process.env.PR_TITLE; // Only if passed from workflow
92
+ }
93
+ return undefined;
94
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadlines",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "Threadline CLI - AI-powered linter based on your natural language documentation",
5
5
  "main": "dist/index.js",
6
6
  "bin": {