threadlines 0.1.6 → 0.1.8

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.
@@ -39,6 +39,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.checkCommand = checkCommand;
40
40
  const experts_1 = require("../validators/experts");
41
41
  const diff_1 = require("../git/diff");
42
+ const file_1 = require("../git/file");
43
+ const repo_1 = require("../git/repo");
44
+ const ci_detection_1 = require("../utils/ci-detection");
42
45
  const client_1 = require("../api/client");
43
46
  const config_1 = require("../utils/config");
44
47
  const fs = __importStar(require("fs"));
@@ -60,6 +63,19 @@ async function checkCommand(options) {
60
63
  console.log(chalk_1.default.gray('For CI/CD: Set THREADLINE_API_KEY as an environment variable in your platform settings.'));
61
64
  process.exit(1);
62
65
  }
66
+ // Get and validate account key
67
+ const account = (0, config_1.getThreadlineAccount)();
68
+ if (!account) {
69
+ console.error(chalk_1.default.red('❌ Error: THREADLINE_ACCOUNT is required'));
70
+ console.log('');
71
+ console.log(chalk_1.default.yellow('To fix this:'));
72
+ console.log(chalk_1.default.white(' 1. Create a .env.local file in your project root'));
73
+ console.log(chalk_1.default.gray(' 2. Add: THREADLINE_ACCOUNT=your-email@example.com'));
74
+ console.log(chalk_1.default.gray(' 3. Make sure .env.local is in your .gitignore'));
75
+ console.log('');
76
+ console.log(chalk_1.default.gray('For CI/CD: Set THREADLINE_ACCOUNT as an environment variable in your platform settings.'));
77
+ process.exit(1);
78
+ }
63
79
  try {
64
80
  // 1. Find and validate threadlines
65
81
  console.log(chalk_1.default.gray('📋 Finding threadlines...'));
@@ -70,9 +86,76 @@ async function checkCommand(options) {
70
86
  console.log(chalk_1.default.gray(' Run `npx threadlines init` to create your first threadline.'));
71
87
  process.exit(0);
72
88
  }
73
- // 2. Get git diff
74
- console.log(chalk_1.default.gray('📝 Collecting git changes...'));
75
- const gitDiff = await (0, diff_1.getGitDiff)(repoRoot);
89
+ // 2. Determine review target and get git diff
90
+ let gitDiff;
91
+ let reviewContext = { type: 'local' };
92
+ // Validate mutually exclusive flags
93
+ const explicitFlags = [options.branch, options.commit, options.file, options.folder, options.files].filter(Boolean);
94
+ if (explicitFlags.length > 1) {
95
+ console.error(chalk_1.default.red('❌ Error: Only one review option can be specified at a time'));
96
+ console.log(chalk_1.default.gray(' Options: --branch, --commit, --file, --folder, --files'));
97
+ process.exit(1);
98
+ }
99
+ // Check for explicit flags first (override auto-detection)
100
+ if (options.file) {
101
+ console.log(chalk_1.default.gray(`📝 Reading file: ${options.file}...`));
102
+ gitDiff = await (0, file_1.getFileContent)(repoRoot, options.file);
103
+ reviewContext = { type: 'file', value: options.file };
104
+ }
105
+ else if (options.folder) {
106
+ console.log(chalk_1.default.gray(`📝 Reading folder: ${options.folder}...`));
107
+ gitDiff = await (0, file_1.getFolderContent)(repoRoot, options.folder);
108
+ reviewContext = { type: 'folder', value: options.folder };
109
+ }
110
+ else if (options.files && options.files.length > 0) {
111
+ console.log(chalk_1.default.gray(`📝 Reading ${options.files.length} file(s)...`));
112
+ gitDiff = await (0, file_1.getMultipleFilesContent)(repoRoot, options.files);
113
+ reviewContext = { type: 'files', value: options.files.join(', ') };
114
+ }
115
+ else if (options.branch) {
116
+ console.log(chalk_1.default.gray(`📝 Collecting git changes for branch: ${options.branch}...`));
117
+ gitDiff = await (0, diff_1.getBranchDiff)(repoRoot, options.branch);
118
+ reviewContext = { type: 'branch', value: options.branch };
119
+ }
120
+ else if (options.commit) {
121
+ console.log(chalk_1.default.gray(`📝 Collecting git changes for commit: ${options.commit}...`));
122
+ gitDiff = await (0, diff_1.getCommitDiff)(repoRoot, options.commit);
123
+ reviewContext = { type: 'commit', value: options.commit };
124
+ }
125
+ else {
126
+ // Auto-detect CI environment or use local changes
127
+ const autoTarget = (0, ci_detection_1.getAutoReviewTarget)();
128
+ if (autoTarget) {
129
+ if (autoTarget.type === 'pr' || autoTarget.type === 'mr') {
130
+ // PR/MR: use source and target branches
131
+ console.log(chalk_1.default.gray(`📝 Collecting git changes for ${autoTarget.type.toUpperCase()}: ${autoTarget.value}...`));
132
+ gitDiff = await (0, diff_1.getPRMRDiff)(repoRoot, autoTarget.sourceBranch, autoTarget.targetBranch);
133
+ reviewContext = { type: autoTarget.type, value: autoTarget.value };
134
+ }
135
+ else if (autoTarget.type === 'branch') {
136
+ // Branch: use branch vs base
137
+ console.log(chalk_1.default.gray(`📝 Collecting git changes for branch: ${autoTarget.value}...`));
138
+ gitDiff = await (0, diff_1.getBranchDiff)(repoRoot, autoTarget.value);
139
+ reviewContext = { type: 'branch', value: autoTarget.value };
140
+ }
141
+ else if (autoTarget.type === 'commit') {
142
+ // Commit: use single commit
143
+ console.log(chalk_1.default.gray(`📝 Collecting git changes for commit: ${autoTarget.value}...`));
144
+ gitDiff = await (0, diff_1.getCommitDiff)(repoRoot, autoTarget.value);
145
+ reviewContext = { type: 'commit', value: autoTarget.value };
146
+ }
147
+ else {
148
+ // Fallback: local development
149
+ console.log(chalk_1.default.gray('📝 Collecting git changes...'));
150
+ gitDiff = await (0, diff_1.getGitDiff)(repoRoot);
151
+ }
152
+ }
153
+ else {
154
+ // Local development: use staged/unstaged changes
155
+ console.log(chalk_1.default.gray('📝 Collecting git changes...'));
156
+ gitDiff = await (0, diff_1.getGitDiff)(repoRoot);
157
+ }
158
+ }
76
159
  if (gitDiff.changedFiles.length === 0) {
77
160
  console.log(chalk_1.default.yellow('⚠️ No changes detected. Make some code changes and try again.'));
78
161
  process.exit(0);
@@ -109,19 +192,28 @@ async function checkCommand(options) {
109
192
  contextContent
110
193
  };
111
194
  });
112
- // 4. Get API URL
113
- const apiUrl = options.apiUrl || process.env.THREADLINE_API_URL || 'http://localhost:3000';
114
- // 5. Call review API
195
+ // 4. Get repo name and branch name
196
+ const repoName = await (0, repo_1.getRepoName)(repoRoot);
197
+ const branchName = await (0, repo_1.getBranchName)(repoRoot);
198
+ // 5. Get API URL (auto-detect Vercel if available)
199
+ const apiUrl = options.apiUrl ||
200
+ process.env.THREADLINE_API_URL ||
201
+ (process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : null) ||
202
+ 'http://localhost:3000';
203
+ // 6. Call review API
115
204
  console.log(chalk_1.default.gray('🤖 Running threadline checks...'));
116
205
  const client = new client_1.ReviewAPIClient(apiUrl);
117
206
  const response = await client.review({
118
207
  threadlines: threadlinesWithContext,
119
208
  diff: gitDiff.diff,
120
209
  files: gitDiff.changedFiles,
121
- apiKey
210
+ apiKey,
211
+ account,
212
+ repoName: repoName || undefined,
213
+ branchName: branchName || undefined
122
214
  });
123
- // 6. Display results
124
- displayResults(response);
215
+ // 7. Display results (with filtering if --full not specified)
216
+ displayResults(response, options.full || false);
125
217
  // Exit with appropriate code
126
218
  const hasAttention = response.results.some(r => r.status === 'attention');
127
219
  process.exit(hasAttention ? 1 : 0);
@@ -131,8 +223,12 @@ async function checkCommand(options) {
131
223
  process.exit(1);
132
224
  }
133
225
  }
134
- function displayResults(response) {
226
+ function displayResults(response, showFull) {
135
227
  const { results, metadata, message } = response;
228
+ // Filter results based on --full flag
229
+ const filteredResults = showFull
230
+ ? results
231
+ : results.filter((r) => r.status === 'attention');
136
232
  // Display informational message if present (e.g., zero diffs)
137
233
  if (message) {
138
234
  console.log('\n' + chalk_1.default.blue('ℹ️ ' + message));
@@ -142,14 +238,23 @@ function displayResults(response) {
142
238
  const notRelevant = results.filter((r) => r.status === 'not_relevant').length;
143
239
  const compliant = results.filter((r) => r.status === 'compliant').length;
144
240
  const attention = results.filter((r) => r.status === 'attention').length;
145
- if (notRelevant > 0) {
146
- console.log(chalk_1.default.gray(` ${notRelevant} not relevant`));
147
- }
148
- if (compliant > 0) {
149
- console.log(chalk_1.default.green(` ${compliant} compliant`));
241
+ if (showFull) {
242
+ // Show all results when --full flag is used
243
+ if (notRelevant > 0) {
244
+ console.log(chalk_1.default.gray(` ${notRelevant} not relevant`));
245
+ }
246
+ if (compliant > 0) {
247
+ console.log(chalk_1.default.green(` ${compliant} compliant`));
248
+ }
249
+ if (attention > 0) {
250
+ console.log(chalk_1.default.yellow(` ${attention} attention`));
251
+ }
150
252
  }
151
- if (attention > 0) {
152
- console.log(chalk_1.default.yellow(` ${attention} attention`));
253
+ else {
254
+ // Default: only show attention items
255
+ if (attention > 0) {
256
+ console.log(chalk_1.default.yellow(` ${attention} attention`));
257
+ }
153
258
  }
154
259
  if (metadata.timedOut > 0) {
155
260
  console.log(chalk_1.default.yellow(` ${metadata.timedOut} timed out`));
@@ -159,7 +264,7 @@ function displayResults(response) {
159
264
  }
160
265
  console.log('');
161
266
  // Show attention items
162
- const attentionItems = results.filter((r) => r.status === 'attention');
267
+ const attentionItems = filteredResults.filter((r) => r.status === 'attention');
163
268
  if (attentionItems.length > 0) {
164
269
  for (const item of attentionItems) {
165
270
  console.log(chalk_1.default.yellow(`⚠️ ${item.expertId}`));
@@ -176,8 +281,15 @@ function displayResults(response) {
176
281
  }
177
282
  console.log('');
178
283
  }
179
- // Show compliant items (optional, can be verbose)
180
- if (attentionItems.length === 0 && compliant > 0) {
284
+ // Show compliant items (only when --full flag is used)
285
+ if (showFull) {
286
+ const compliantItems = filteredResults.filter((r) => r.status === 'compliant');
287
+ if (compliantItems.length > 0 && attentionItems.length === 0) {
288
+ console.log(chalk_1.default.green('✓ All threadlines passed!\n'));
289
+ }
290
+ }
291
+ else if (attentionItems.length === 0 && compliant > 0) {
292
+ // Default: show success message if no attention items
181
293
  console.log(chalk_1.default.green('✓ All threadlines passed!\n'));
182
294
  }
183
295
  }
@@ -95,11 +95,12 @@ async function initCommand() {
95
95
  console.log(chalk_1.default.gray(' 1. Edit threadlines/example.md with your coding standards'));
96
96
  console.log(chalk_1.default.gray(' 2. Rename it to something descriptive (e.g., error-handling.md)'));
97
97
  console.log('');
98
- console.log(chalk_1.default.yellow('⚠️ IMPORTANT: API Key Setup Required'));
99
- console.log(chalk_1.default.white(' To use threadlines check, you need a THREADLINE_API_KEY.'));
98
+ console.log(chalk_1.default.yellow('⚠️ IMPORTANT: Configuration Required'));
99
+ console.log(chalk_1.default.white(' To use threadlines check, you need:'));
100
100
  console.log('');
101
101
  console.log(chalk_1.default.white(' Create a .env.local file in your project root with:'));
102
102
  console.log(chalk_1.default.gray(' THREADLINE_API_KEY=your-api-key-here'));
103
+ console.log(chalk_1.default.gray(' THREADLINE_ACCOUNT=your-email@example.com'));
103
104
  console.log('');
104
105
  console.log(chalk_1.default.white(' Make sure .env.local is in your .gitignore file!'));
105
106
  console.log('');
package/dist/git/diff.js CHANGED
@@ -4,7 +4,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getGitDiff = getGitDiff;
7
+ exports.getBranchDiff = getBranchDiff;
8
+ exports.getCommitDiff = getCommitDiff;
9
+ exports.getPRMRDiff = getPRMRDiff;
7
10
  const simple_git_1 = __importDefault(require("simple-git"));
11
+ /**
12
+ * Get diff for staged/unstaged changes (current behavior)
13
+ */
8
14
  async function getGitDiff(repoRoot) {
9
15
  const git = (0, simple_git_1.default)(repoRoot);
10
16
  // Check if we're in a git repo
@@ -39,3 +45,164 @@ async function getGitDiff(repoRoot) {
39
45
  changedFiles
40
46
  };
41
47
  }
48
+ /**
49
+ * Get diff for a specific branch (all commits vs base branch)
50
+ * Uses git merge-base to find common ancestor, then diffs from there
51
+ */
52
+ async function getBranchDiff(repoRoot, branchName, baseBranch) {
53
+ const git = (0, simple_git_1.default)(repoRoot);
54
+ // Check if we're in a git repo
55
+ const isRepo = await git.checkIsRepo();
56
+ if (!isRepo) {
57
+ throw new Error('Not a git repository. Threadline requires a git repository.');
58
+ }
59
+ // Determine base branch
60
+ let base;
61
+ if (baseBranch) {
62
+ // Use provided base branch
63
+ base = baseBranch;
64
+ }
65
+ else {
66
+ // Check if the branch itself is a base branch (main/master)
67
+ const baseBranchNames = ['main', 'master'];
68
+ const isBaseBranch = baseBranchNames.includes(branchName.toLowerCase());
69
+ if (isBaseBranch) {
70
+ // For main/master branch, compare against previous commit (HEAD~1)
71
+ // This checks what changed in the most recent commit
72
+ try {
73
+ const previousCommit = await git.revparse(['HEAD~1']).catch(() => null);
74
+ if (previousCommit) {
75
+ // Use commit-based diff instead
76
+ const diff = await git.diff([`${previousCommit}..HEAD`]);
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
+ };
83
+ }
84
+ }
85
+ catch {
86
+ // If no previous commit, return empty (first commit)
87
+ return {
88
+ diff: '',
89
+ changedFiles: []
90
+ };
91
+ }
92
+ }
93
+ // Try to detect base branch: upstream, default branch, or common names
94
+ base = await detectBaseBranch(git, branchName);
95
+ }
96
+ // Helper function to detect base branch
97
+ async function detectBaseBranch(git, branchName) {
98
+ // Try upstream tracking branch
99
+ const upstream = await git.revparse(['--abbrev-ref', '--symbolic-full-name', `${branchName}@{u}`]).catch(() => null);
100
+ if (upstream) {
101
+ // Extract base from upstream (e.g., "origin/main" -> "main")
102
+ const upstreamBranch = upstream.replace(/^origin\//, '');
103
+ // Don't use the branch itself as its base
104
+ if (upstreamBranch !== branchName) {
105
+ return upstreamBranch;
106
+ }
107
+ }
108
+ // Try default branch
109
+ try {
110
+ const defaultBranch = await git.revparse(['--abbrev-ref', 'refs/remotes/origin/HEAD']);
111
+ const defaultBranchName = defaultBranch.replace(/^origin\//, '');
112
+ // Don't use the branch itself as its base
113
+ if (defaultBranchName !== branchName) {
114
+ return defaultBranchName;
115
+ }
116
+ }
117
+ catch {
118
+ // Continue to fallback
119
+ }
120
+ // Fallback to common names (excluding the branch itself)
121
+ const commonBases = ['main', 'master', 'develop'];
122
+ for (const candidate of commonBases) {
123
+ if (candidate.toLowerCase() === branchName.toLowerCase()) {
124
+ continue; // Skip if it's the same branch
125
+ }
126
+ try {
127
+ await git.revparse([`origin/${candidate}`]);
128
+ return candidate;
129
+ }
130
+ catch {
131
+ // Try next
132
+ }
133
+ }
134
+ throw new Error(`Could not determine base branch for '${branchName}'. Please specify with --base flag or set upstream tracking.`);
135
+ }
136
+ // Get diff between base and branch (cumulative diff of all commits)
137
+ // Format: git diff base...branch (three-dot notation finds common ancestor)
138
+ const diff = await git.diff([`${base}...${branchName}`]);
139
+ // Get list of changed files
140
+ const diffSummary = await git.diffSummary([`${base}...${branchName}`]);
141
+ const changedFiles = diffSummary.files.map(f => f.file);
142
+ return {
143
+ diff: diff || '',
144
+ changedFiles
145
+ };
146
+ }
147
+ /**
148
+ * Get diff for a specific commit
149
+ */
150
+ async function getCommitDiff(repoRoot, sha) {
151
+ const git = (0, simple_git_1.default)(repoRoot);
152
+ // Check if we're in a git repo
153
+ const isRepo = await git.checkIsRepo();
154
+ if (!isRepo) {
155
+ throw new Error('Not a git repository. Threadline requires a git repository.');
156
+ }
157
+ // Get diff for the commit
158
+ // Use git show to get the commit diff
159
+ let diff;
160
+ let changedFiles;
161
+ try {
162
+ // Get diff using git show
163
+ diff = await git.show([sha, '--format=', '--no-color']);
164
+ // Get changed files using git show --name-only
165
+ const commitFiles = await git.show([sha, '--name-only', '--format=', '--pretty=format:']);
166
+ changedFiles = commitFiles
167
+ .split('\n')
168
+ .filter(line => line.trim().length > 0)
169
+ .map(line => line.trim());
170
+ }
171
+ catch (error) {
172
+ // Fallback: try git diff format
173
+ try {
174
+ diff = await git.diff([`${sha}^..${sha}`]);
175
+ // Get files from diff summary
176
+ const diffSummary = await git.diffSummary([`${sha}^..${sha}`]);
177
+ changedFiles = diffSummary.files.map(f => f.file);
178
+ }
179
+ catch (diffError) {
180
+ throw new Error(`Commit ${sha} not found or invalid: ${error.message || diffError.message}`);
181
+ }
182
+ }
183
+ return {
184
+ diff: diff || '',
185
+ changedFiles
186
+ };
187
+ }
188
+ /**
189
+ * Get diff for PR/MR (source branch vs target branch)
190
+ */
191
+ async function getPRMRDiff(repoRoot, sourceBranch, targetBranch) {
192
+ const git = (0, simple_git_1.default)(repoRoot);
193
+ // Check if we're in a git repo
194
+ const isRepo = await git.checkIsRepo();
195
+ if (!isRepo) {
196
+ throw new Error('Not a git repository. Threadline requires a git repository.');
197
+ }
198
+ // Get diff between target and source (cumulative diff)
199
+ // Format: git diff target...source (three-dot notation finds common ancestor)
200
+ const diff = await git.diff([`${targetBranch}...${sourceBranch}`]);
201
+ // Get list of changed files
202
+ const diffSummary = await git.diffSummary([`${targetBranch}...${sourceBranch}`]);
203
+ const changedFiles = diffSummary.files.map(f => f.file);
204
+ return {
205
+ diff: diff || '',
206
+ changedFiles
207
+ };
208
+ }
@@ -0,0 +1,159 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getFileContent = getFileContent;
37
+ exports.getFolderContent = getFolderContent;
38
+ exports.getMultipleFilesContent = getMultipleFilesContent;
39
+ const fs = __importStar(require("fs"));
40
+ const path = __importStar(require("path"));
41
+ const glob_1 = require("glob");
42
+ /**
43
+ * Read content of a single file and create artificial diff (all lines as additions)
44
+ */
45
+ async function getFileContent(repoRoot, filePath) {
46
+ const fullPath = path.resolve(repoRoot, filePath);
47
+ // Check if file exists
48
+ if (!fs.existsSync(fullPath)) {
49
+ throw new Error(`File '${filePath}' not found`);
50
+ }
51
+ // Check if it's actually a file (not a directory)
52
+ const stats = fs.statSync(fullPath);
53
+ if (!stats.isFile()) {
54
+ throw new Error(`Path '${filePath}' is not a file`);
55
+ }
56
+ // Read file content
57
+ const content = fs.readFileSync(fullPath, 'utf-8');
58
+ // Create artificial diff (all lines as additions)
59
+ const lines = content.split('\n');
60
+ const diff = lines.map((line, index) => `+${line}`).join('\n');
61
+ // Add diff header
62
+ const diffHeader = `--- /dev/null\n+++ ${filePath}\n@@ -0,0 +1,${lines.length} @@\n`;
63
+ const fullDiff = diffHeader + diff;
64
+ return {
65
+ diff: fullDiff,
66
+ changedFiles: [filePath]
67
+ };
68
+ }
69
+ /**
70
+ * Read content of all files in a folder (recursively) and create artificial diff
71
+ */
72
+ async function getFolderContent(repoRoot, folderPath) {
73
+ const fullPath = path.resolve(repoRoot, folderPath);
74
+ // Check if folder exists
75
+ if (!fs.existsSync(fullPath)) {
76
+ throw new Error(`Folder '${folderPath}' not found`);
77
+ }
78
+ // Check if it's actually a directory
79
+ const stats = fs.statSync(fullPath);
80
+ if (!stats.isDirectory()) {
81
+ throw new Error(`Path '${folderPath}' is not a folder`);
82
+ }
83
+ // Find all files recursively
84
+ const pattern = path.join(fullPath, '**', '*');
85
+ const files = await (0, glob_1.glob)(pattern, {
86
+ cwd: repoRoot,
87
+ absolute: false,
88
+ ignore: ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**']
89
+ });
90
+ // Filter to only actual files (not directories)
91
+ const filePaths = files.filter(file => {
92
+ const fileFullPath = path.resolve(repoRoot, file);
93
+ try {
94
+ return fs.statSync(fileFullPath).isFile();
95
+ }
96
+ catch {
97
+ return false;
98
+ }
99
+ });
100
+ if (filePaths.length === 0) {
101
+ throw new Error(`No files found in folder '${folderPath}'`);
102
+ }
103
+ // Read all files and create combined diff
104
+ const diffs = [];
105
+ const changedFiles = [];
106
+ for (const filePath of filePaths) {
107
+ try {
108
+ const content = fs.readFileSync(path.resolve(repoRoot, filePath), 'utf-8');
109
+ const lines = content.split('\n');
110
+ // Create artificial diff for this file
111
+ const fileDiff = lines.map((line, index) => `+${line}`).join('\n');
112
+ const diffHeader = `--- /dev/null\n+++ ${filePath}\n@@ -0,0 +1,${lines.length} @@\n`;
113
+ diffs.push(diffHeader + fileDiff);
114
+ changedFiles.push(filePath);
115
+ }
116
+ catch (error) {
117
+ // Skip files that can't be read (permissions, etc.)
118
+ console.warn(`Warning: Could not read file '${filePath}': ${error.message}`);
119
+ }
120
+ }
121
+ return {
122
+ diff: diffs.join('\n'),
123
+ changedFiles
124
+ };
125
+ }
126
+ /**
127
+ * Read content of multiple specified files and create artificial diff
128
+ */
129
+ async function getMultipleFilesContent(repoRoot, filePaths) {
130
+ if (filePaths.length === 0) {
131
+ throw new Error('No files specified');
132
+ }
133
+ const diffs = [];
134
+ const changedFiles = [];
135
+ for (const filePath of filePaths) {
136
+ const fullPath = path.resolve(repoRoot, filePath);
137
+ // Check if file exists
138
+ if (!fs.existsSync(fullPath)) {
139
+ throw new Error(`File '${filePath}' not found`);
140
+ }
141
+ // Check if it's actually a file
142
+ const stats = fs.statSync(fullPath);
143
+ if (!stats.isFile()) {
144
+ throw new Error(`Path '${filePath}' is not a file`);
145
+ }
146
+ // Read file content
147
+ const content = fs.readFileSync(fullPath, 'utf-8');
148
+ const lines = content.split('\n');
149
+ // Create artificial diff for this file
150
+ const fileDiff = lines.map((line, index) => `+${line}`).join('\n');
151
+ const diffHeader = `--- /dev/null\n+++ ${filePath}\n@@ -0,0 +1,${lines.length} @@\n`;
152
+ diffs.push(diffHeader + fileDiff);
153
+ changedFiles.push(filePath);
154
+ }
155
+ return {
156
+ diff: diffs.join('\n'),
157
+ changedFiles
158
+ };
159
+ }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getRepoName = getRepoName;
7
+ exports.getBranchName = getBranchName;
8
+ const simple_git_1 = __importDefault(require("simple-git"));
9
+ /**
10
+ * Gets repository name from git remote URL.
11
+ * Parses common formats: github.com/user/repo, gitlab.com/user/repo, etc.
12
+ * Returns null if no remote or parsing fails.
13
+ */
14
+ async function getRepoName(repoRoot) {
15
+ const git = (0, simple_git_1.default)(repoRoot);
16
+ try {
17
+ const remotes = await git.getRemotes(true);
18
+ const origin = remotes.find(r => r.name === 'origin');
19
+ if (!origin?.refs?.fetch) {
20
+ return null;
21
+ }
22
+ const url = origin.refs.fetch;
23
+ // Parse repo name from common URL formats:
24
+ // - https://github.com/user/repo.git
25
+ // - git@github.com:user/repo.git
26
+ // - https://gitlab.com/user/repo.git
27
+ // - git@gitlab.com:user/repo.git
28
+ const patterns = [
29
+ /(?:github\.com|gitlab\.com)[\/:]([^\/]+\/[^\/]+?)(?:\.git)?$/,
30
+ /([^\/]+\/[^\/]+?)(?:\.git)?$/
31
+ ];
32
+ for (const pattern of patterns) {
33
+ const match = url.match(pattern);
34
+ if (match && match[1]) {
35
+ return match[1];
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+ catch (error) {
41
+ // If no remote or error, return null
42
+ return null;
43
+ }
44
+ }
45
+ /**
46
+ * Gets current branch name from git.
47
+ * Returns null if not in a git repo or error occurs.
48
+ */
49
+ async function getBranchName(repoRoot) {
50
+ const git = (0, simple_git_1.default)(repoRoot);
51
+ try {
52
+ const branch = await git.revparse(['--abbrev-ref', 'HEAD']);
53
+ return branch || null;
54
+ }
55
+ catch (error) {
56
+ return null;
57
+ }
58
+ }
package/dist/index.js CHANGED
@@ -61,6 +61,12 @@ program
61
61
  program
62
62
  .command('check')
63
63
  .description('Check code against your threadlines')
64
- .option('--api-url <url>', 'Threadline server URL', process.env.THREADLINE_API_URL || 'http://localhost:3000')
64
+ .option('--api-url <url>', 'Threadline server URL', process.env.THREADLINE_API_URL || (process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : 'http://localhost:3000'))
65
+ .option('--full', 'Show all results (compliant, attention, not_relevant). Default: only attention items')
66
+ .option('--branch <name>', 'Review all commits in branch vs base')
67
+ .option('--commit <sha>', 'Review specific commit')
68
+ .option('--file <path>', 'Review entire file (all lines as additions)')
69
+ .option('--folder <path>', 'Review all files in folder recursively')
70
+ .option('--files <paths...>', 'Review multiple specified files')
65
71
  .action(check_1.checkCommand);
66
72
  program.parse();
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getAutoReviewTarget = getAutoReviewTarget;
4
+ function getAutoReviewTarget() {
5
+ // 1. Check for PR/MR context (most authoritative)
6
+ // GitHub Actions PR
7
+ if (process.env.GITHUB_EVENT_NAME === 'pull_request') {
8
+ const targetBranch = process.env.GITHUB_BASE_REF;
9
+ const sourceBranch = process.env.GITHUB_HEAD_REF;
10
+ const prNumber = process.env.GITHUB_EVENT_PULL_REQUEST_NUMBER;
11
+ if (targetBranch && sourceBranch && prNumber) {
12
+ return {
13
+ type: 'pr',
14
+ value: prNumber,
15
+ sourceBranch,
16
+ targetBranch
17
+ };
18
+ }
19
+ }
20
+ // GitLab CI MR
21
+ if (process.env.CI_MERGE_REQUEST_IID) {
22
+ const targetBranch = process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME;
23
+ const sourceBranch = process.env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME;
24
+ const mrNumber = process.env.CI_MERGE_REQUEST_IID;
25
+ if (targetBranch && sourceBranch && mrNumber) {
26
+ return {
27
+ type: 'mr',
28
+ value: mrNumber,
29
+ sourceBranch,
30
+ targetBranch
31
+ };
32
+ }
33
+ }
34
+ // 2. Check for branch name (CI with branch)
35
+ const branch = process.env.GITHUB_REF_NAME || // GitHub Actions
36
+ process.env.CI_COMMIT_REF_NAME || // GitLab CI
37
+ process.env.VERCEL_GIT_COMMIT_REF; // Vercel
38
+ if (branch) {
39
+ return {
40
+ type: 'branch',
41
+ value: branch
42
+ };
43
+ }
44
+ // 3. Check for commit SHA (CI without branch)
45
+ const commit = process.env.GITHUB_SHA || // GitHub Actions
46
+ process.env.CI_COMMIT_SHA || // GitLab CI
47
+ process.env.VERCEL_GIT_COMMIT_SHA; // Vercel
48
+ if (commit) {
49
+ return {
50
+ type: 'commit',
51
+ value: commit
52
+ };
53
+ }
54
+ // 4. Local development (no CI env vars)
55
+ return null;
56
+ }
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.getThreadlineApiKey = getThreadlineApiKey;
40
+ exports.getThreadlineAccount = getThreadlineAccount;
40
41
  const fs = __importStar(require("fs"));
41
42
  const path = __importStar(require("path"));
42
43
  const dotenv_1 = __importDefault(require("dotenv"));
@@ -61,3 +62,13 @@ function getThreadlineApiKey() {
61
62
  // Check environment variable (from shell or CI/CD)
62
63
  return process.env.THREADLINE_API_KEY;
63
64
  }
65
+ /**
66
+ * Gets THREADLINE_ACCOUNT from environment.
67
+ * Priority: process.env.THREADLINE_ACCOUNT → .env.local file
68
+ */
69
+ function getThreadlineAccount() {
70
+ // Load .env.local if it exists (doesn't override existing env vars)
71
+ loadEnvLocal();
72
+ // Check environment variable (from shell or CI/CD)
73
+ return process.env.THREADLINE_ACCOUNT;
74
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadlines",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Threadline CLI - AI-powered linter based on your natural language documentation",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -33,16 +33,18 @@
33
33
  "prepublishOnly": "npm run build"
34
34
  },
35
35
  "dependencies": {
36
- "commander": "^12.1.0",
37
- "dotenv": "^16.4.7",
38
- "simple-git": "^3.27.0",
39
36
  "axios": "^1.7.9",
40
37
  "chalk": "^4.1.2",
41
- "js-yaml": "^4.1.0"
38
+ "commander": "^12.1.0",
39
+ "dotenv": "^16.4.7",
40
+ "glob": "^13.0.0",
41
+ "js-yaml": "^4.1.0",
42
+ "simple-git": "^3.27.0"
42
43
  },
43
44
  "devDependencies": {
44
- "@types/node": "^22.10.2",
45
+ "@types/glob": "^8.1.0",
45
46
  "@types/js-yaml": "^4.0.9",
47
+ "@types/node": "^22.10.2",
46
48
  "typescript": "^5.7.2"
47
49
  }
48
50
  }