threadlines 0.1.23 → 0.1.25

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.
@@ -52,30 +52,39 @@ const chalk_1 = __importDefault(require("chalk"));
52
52
  async function checkCommand(options) {
53
53
  const repoRoot = process.cwd();
54
54
  console.log(chalk_1.default.blue('🔍 Threadline: Checking code against your threadlines...\n'));
55
- // Get and validate API key
55
+ // Pre-flight check: Validate ALL required environment variables at once
56
56
  const apiKey = (0, config_1.getThreadlineApiKey)();
57
- if (!apiKey) {
58
- console.error(chalk_1.default.red('❌ Error: THREADLINE_API_KEY is required'));
57
+ const account = (0, config_1.getThreadlineAccount)();
58
+ const missingVars = [];
59
+ if (!apiKey)
60
+ missingVars.push('THREADLINE_API_KEY');
61
+ if (!account)
62
+ missingVars.push('THREADLINE_ACCOUNT');
63
+ if (missingVars.length > 0) {
64
+ console.error(chalk_1.default.red('❌ Error: Missing required environment variables:'));
65
+ for (const varName of missingVars) {
66
+ console.error(chalk_1.default.red(` • ${varName}`));
67
+ }
59
68
  console.log('');
60
69
  console.log(chalk_1.default.yellow('To fix this:'));
61
- console.log(chalk_1.default.white(' 1. Create a .env.local file in your project root'));
62
- console.log(chalk_1.default.gray(' 2. Add: THREADLINE_API_KEY=your-api-key-here'));
63
- console.log(chalk_1.default.gray(' 3. Make sure .env.local is in your .gitignore'));
64
70
  console.log('');
65
- console.log(chalk_1.default.gray('For CI/CD: Set THREADLINE_API_KEY as an environment variable in your platform settings.'));
66
- process.exit(1);
67
- }
68
- // Get and validate account key
69
- const account = (0, config_1.getThreadlineAccount)();
70
- if (!account) {
71
- console.error(chalk_1.default.red('❌ Error: THREADLINE_ACCOUNT is required'));
71
+ console.log(chalk_1.default.white(' Local development:'));
72
+ console.log(chalk_1.default.gray(' 1. Create a .env.local file in your project root'));
73
+ console.log(chalk_1.default.gray(' 2. Add the missing variable(s):'));
74
+ if (missingVars.includes('THREADLINE_API_KEY')) {
75
+ console.log(chalk_1.default.gray(' THREADLINE_API_KEY=your-api-key-here'));
76
+ }
77
+ if (missingVars.includes('THREADLINE_ACCOUNT')) {
78
+ console.log(chalk_1.default.gray(' THREADLINE_ACCOUNT=your-email@example.com'));
79
+ }
80
+ console.log(chalk_1.default.gray(' 3. Make sure .env.local is in your .gitignore'));
72
81
  console.log('');
73
- console.log(chalk_1.default.yellow('To fix this:'));
74
- console.log(chalk_1.default.white(' 1. Create a .env.local file in your project root'));
75
- console.log(chalk_1.default.gray(' 2. Add: THREADLINE_ACCOUNT=your-email@example.com'));
76
- console.log(chalk_1.default.gray(' 3. Make sure .env.local is in your .gitignore'));
82
+ console.log(chalk_1.default.white(' CI/CD:'));
83
+ console.log(chalk_1.default.gray(' GitHub Actions: Settings Secrets Add variables'));
84
+ console.log(chalk_1.default.gray(' GitLab CI: Settings → CI/CD → Variables'));
85
+ console.log(chalk_1.default.gray(' Vercel: Settings Environment Variables'));
77
86
  console.log('');
78
- console.log(chalk_1.default.gray('For CI/CD: Set THREADLINE_ACCOUNT as an environment variable in your platform settings.'));
87
+ console.log(chalk_1.default.gray('Get your credentials at: https://devthreadline.com/settings'));
79
88
  process.exit(1);
80
89
  }
81
90
  try {
@@ -216,8 +225,8 @@ async function checkCommand(options) {
216
225
  threadlines: threadlinesWithContext,
217
226
  diff: gitDiff.diff,
218
227
  files: gitDiff.changedFiles,
219
- apiKey,
220
- account,
228
+ apiKey: apiKey,
229
+ account: account,
221
230
  repoName: repoName,
222
231
  branchName: branchName,
223
232
  commitSha: metadata.commitSha,
@@ -16,6 +16,8 @@ const repo_2 = require("./repo");
16
16
  * Each environment has a single, specific implementation:
17
17
  * - GitHub: Uses GITHUB_REPOSITORY, GITHUB_REF_NAME, and GitHub-specific diff logic
18
18
  * - Vercel: Uses VERCEL_GIT_REPO_OWNER/SLUG, VERCEL_GIT_COMMIT_REF, and Vercel-specific diff logic
19
+ * - GitLab: Uses CI_PROJECT_URL, CI_COMMIT_REF_NAME, and GitLab-specific diff logic
20
+ * (fetches default branch on-demand since GitLab only clones current branch)
19
21
  * - Local: Uses git commands for repo/branch, and local diff logic
20
22
  *
21
23
  * All methods fail loudly if they can't get the required information.
@@ -41,8 +43,11 @@ async function getGitContextForEnvironment(environment, repoRoot) {
41
43
  branchName: await (0, repo_2.getLocalBranchName)(repoRoot)
42
44
  };
43
45
  case 'gitlab':
44
- // GitLab not implemented yet - will be added later
45
- throw new Error('GitLab environment not yet supported for unified git context collection.');
46
+ return {
47
+ diff: await (0, git_diff_executor_1.getDiffForEnvironment)('gitlab', repoRoot),
48
+ repoName: await (0, repo_1.getGitLabRepoName)(repoRoot),
49
+ branchName: await (0, repo_2.getGitLabBranchName)(repoRoot)
50
+ };
46
51
  default:
47
52
  const _exhaustive = environment;
48
53
  throw new Error(`Unknown environment: ${_exhaustive}`);
@@ -27,7 +27,13 @@ const repo_1 = require("./repo");
27
27
  * Compare: origin/default~1 vs origin/default
28
28
  * Shows: Changes in the direct commit
29
29
  *
30
- */
30
+ * Known Limitation - Rebase and Merge:
31
+ * When using "Rebase and merge" strategy in GitHub, multiple commits are
32
+ * added to the default branch. Our approach (default~1 vs default) only
33
+ * captures the LAST commit, not all rebased commits. This is a naive
34
+ * implementation. To fully support rebase merges, we'd need to use the
35
+ * `before` SHA from GITHUB_EVENT_PATH to compare before...after.
36
+ */
31
37
  async function getGitHubDiff(repoRoot) {
32
38
  const git = (0, simple_git_1.default)(repoRoot);
33
39
  // Check if we're in a git repo
@@ -8,13 +8,21 @@ const simple_git_1 = __importDefault(require("simple-git"));
8
8
  /**
9
9
  * Get diff for GitLab CI environment
10
10
  *
11
- * GitLab CI provides environment variables that tell us exactly what to compare:
12
- * - MR context: CI_MERGE_REQUEST_TARGET_BRANCH_NAME (target branch) and CI_MERGE_REQUEST_SOURCE_BRANCH_NAME (source branch)
13
- * - Branch context: CI_COMMIT_REF_NAME (current branch), compare against origin/main
11
+ * GitLab CI does a shallow clone of ONLY the current branch. The default branch
12
+ * (e.g., origin/main) is NOT available by default. We fetch it on-demand.
14
13
  *
15
- * This implementation follows the same pattern as GitHub Actions, using GitLab's equivalent
16
- * environment variables. This is the ONLY implementation for GitLab - no fallbacks, no alternatives.
17
- * If this doesn't work, we fail with a clear error.
14
+ * Scenarios handled:
15
+ *
16
+ * 1. MR Context (CI_MERGE_REQUEST_IID is set):
17
+ * - Fetch target branch, then diff target vs source
18
+ *
19
+ * 2. Feature Branch Push (CI_COMMIT_REF_NAME != CI_DEFAULT_BRANCH):
20
+ * - Fetch default branch, then diff default vs feature
21
+ *
22
+ * 3. Default Branch Push (CI_COMMIT_REF_NAME == CI_DEFAULT_BRANCH):
23
+ * - Use HEAD~1...HEAD (last commit only, no fetch needed)
24
+ *
25
+ * This is the ONLY implementation for GitLab - no fallbacks, no alternatives.
18
26
  */
19
27
  async function getGitLabDiff(repoRoot) {
20
28
  const git = (0, simple_git_1.default)(repoRoot);
@@ -23,19 +31,23 @@ async function getGitLabDiff(repoRoot) {
23
31
  if (!isRepo) {
24
32
  throw new Error('Not a git repository. Threadline requires a git repository.');
25
33
  }
26
- // Determine context from GitLab CI environment variables
34
+ // Get GitLab CI environment variables
27
35
  const mrIid = process.env.CI_MERGE_REQUEST_IID;
28
36
  const targetBranch = process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME;
29
37
  const sourceBranch = process.env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME;
30
38
  const refName = process.env.CI_COMMIT_REF_NAME;
31
- // MR context: GitLab provides both target and source branches
39
+ const defaultBranch = process.env.CI_DEFAULT_BRANCH || 'main';
40
+ // Scenario 1: MR Context
32
41
  if (mrIid) {
33
42
  if (!targetBranch || !sourceBranch) {
34
43
  throw new Error('GitLab MR context detected but CI_MERGE_REQUEST_TARGET_BRANCH_NAME or ' +
35
44
  'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME is missing. ' +
36
45
  'This should be automatically provided by GitLab CI.');
37
46
  }
38
- // Use the branches GitLab provides directly - no detection needed
47
+ // Fetch target branch (GitLab doesn't have it by default)
48
+ console.log(` [GitLab] Fetching target branch: origin/${targetBranch}`);
49
+ await git.fetch(['origin', `${targetBranch}:refs/remotes/origin/${targetBranch}`, '--depth=1']);
50
+ // Diff target vs source
39
51
  const diff = await git.diff([`origin/${targetBranch}...origin/${sourceBranch}`, '-U200']);
40
52
  const diffSummary = await git.diffSummary([`origin/${targetBranch}...origin/${sourceBranch}`]);
41
53
  const changedFiles = diffSummary.files.map(f => f.file);
@@ -44,20 +56,31 @@ async function getGitLabDiff(repoRoot) {
44
56
  changedFiles
45
57
  };
46
58
  }
47
- // Branch context: GitLab provides branch name, compare against origin/main
48
- if (refName) {
49
- // For branch pushes, compare against origin/main (standard base branch)
50
- // GitLab CI with fetch-depth: 0 should have origin/main available
51
- const diff = await git.diff([`origin/main...origin/${refName}`, '-U200']);
52
- const diffSummary = await git.diffSummary([`origin/main...origin/${refName}`]);
59
+ // Scenario 2 & 3: Branch Push
60
+ if (!refName) {
61
+ throw new Error('GitLab CI environment detected but CI_COMMIT_REF_NAME is not set. ' +
62
+ 'This should be automatically provided by GitLab CI.');
63
+ }
64
+ // Scenario 3: Default Branch Push (e.g., direct commit to main)
65
+ if (refName === defaultBranch) {
66
+ console.log(` [GitLab] Push to default branch (${defaultBranch}), using HEAD~1...HEAD`);
67
+ const diff = await git.diff(['HEAD~1...HEAD', '-U200']);
68
+ const diffSummary = await git.diffSummary(['HEAD~1...HEAD']);
53
69
  const changedFiles = diffSummary.files.map(f => f.file);
54
70
  return {
55
71
  diff: diff || '',
56
72
  changedFiles
57
73
  };
58
74
  }
59
- // Neither MR nor branch context available
60
- throw new Error('GitLab CI environment detected but no valid context found. ' +
61
- 'Expected CI_MERGE_REQUEST_IID (with CI_MERGE_REQUEST_TARGET_BRANCH_NAME/CI_MERGE_REQUEST_SOURCE_BRANCH_NAME) ' +
62
- 'or CI_COMMIT_REF_NAME for branch context.');
75
+ // Scenario 2: Feature Branch Push
76
+ console.log(` [GitLab] Feature branch push, fetching default branch: origin/${defaultBranch}`);
77
+ await git.fetch(['origin', `${defaultBranch}:refs/remotes/origin/${defaultBranch}`, '--depth=1']);
78
+ // Diff default vs feature
79
+ const diff = await git.diff([`origin/${defaultBranch}...origin/${refName}`, '-U200']);
80
+ const diffSummary = await git.diffSummary([`origin/${defaultBranch}...origin/${refName}`]);
81
+ const changedFiles = diffSummary.files.map(f => f.file);
82
+ return {
83
+ diff: diff || '',
84
+ changedFiles
85
+ };
63
86
  }
package/dist/git/repo.js CHANGED
@@ -42,6 +42,8 @@ exports.getLocalRepoName = getLocalRepoName;
42
42
  exports.getGitHubBranchName = getGitHubBranchName;
43
43
  exports.getVercelBranchName = getVercelBranchName;
44
44
  exports.getLocalBranchName = getLocalBranchName;
45
+ exports.getGitLabRepoName = getGitLabRepoName;
46
+ exports.getGitLabBranchName = getGitLabBranchName;
45
47
  exports.getDefaultBranchName = getDefaultBranchName;
46
48
  const simple_git_1 = __importDefault(require("simple-git"));
47
49
  const fs = __importStar(require("fs"));
@@ -173,6 +175,36 @@ async function getLocalBranchName(repoRoot) {
173
175
  throw new Error(`Local: Failed to get branch name from git: ${error.message}`);
174
176
  }
175
177
  }
178
+ /**
179
+ * GitLab CI: Get repository name
180
+ *
181
+ * Uses CI_PROJECT_URL environment variable.
182
+ * This is the ONLY method for GitLab - no fallbacks, no alternatives.
183
+ */
184
+ async function getGitLabRepoName(repoRoot) {
185
+ const projectUrl = process.env.CI_PROJECT_URL;
186
+ if (!projectUrl) {
187
+ throw new Error('GitLab CI: CI_PROJECT_URL environment variable is not set. ' +
188
+ 'This should be automatically provided by GitLab CI.');
189
+ }
190
+ // CI_PROJECT_URL is like "https://gitlab.com/owner/repo"
191
+ // Add .git suffix for consistency with other environments
192
+ return `${projectUrl}.git`;
193
+ }
194
+ /**
195
+ * GitLab CI: Get branch name
196
+ *
197
+ * Uses CI_COMMIT_REF_NAME environment variable.
198
+ * This is the ONLY method for GitLab - no fallbacks, no alternatives.
199
+ */
200
+ async function getGitLabBranchName(repoRoot) {
201
+ const refName = process.env.CI_COMMIT_REF_NAME;
202
+ if (!refName) {
203
+ throw new Error('GitLab CI: CI_COMMIT_REF_NAME environment variable is not set. ' +
204
+ 'This should be automatically provided by GitLab CI.');
205
+ }
206
+ return refName;
207
+ }
176
208
  /**
177
209
  * Detects the default branch name of the repository for GitHub Actions.
178
210
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadlines",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Threadline CLI - AI-powered linter based on your natural language documentation",
5
5
  "main": "dist/index.js",
6
6
  "bin": {