threadlines 0.1.6 → 0.1.7

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,25 @@ async function checkCommand(options) {
109
192
  contextContent
110
193
  };
111
194
  });
112
- // 4. Get API URL
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
113
199
  const apiUrl = options.apiUrl || process.env.THREADLINE_API_URL || 'http://localhost:3000';
114
- // 5. Call review API
200
+ // 6. Call review API
115
201
  console.log(chalk_1.default.gray('🤖 Running threadline checks...'));
116
202
  const client = new client_1.ReviewAPIClient(apiUrl);
117
203
  const response = await client.review({
118
204
  threadlines: threadlinesWithContext,
119
205
  diff: gitDiff.diff,
120
206
  files: gitDiff.changedFiles,
121
- apiKey
207
+ apiKey,
208
+ account,
209
+ repoName: repoName || undefined,
210
+ branchName: branchName || undefined
122
211
  });
123
- // 6. Display results
124
- displayResults(response);
212
+ // 7. Display results (with filtering if --full not specified)
213
+ displayResults(response, options.full || false);
125
214
  // Exit with appropriate code
126
215
  const hasAttention = response.results.some(r => r.status === 'attention');
127
216
  process.exit(hasAttention ? 1 : 0);
@@ -131,8 +220,12 @@ async function checkCommand(options) {
131
220
  process.exit(1);
132
221
  }
133
222
  }
134
- function displayResults(response) {
223
+ function displayResults(response, showFull) {
135
224
  const { results, metadata, message } = response;
225
+ // Filter results based on --full flag
226
+ const filteredResults = showFull
227
+ ? results
228
+ : results.filter((r) => r.status === 'attention');
136
229
  // Display informational message if present (e.g., zero diffs)
137
230
  if (message) {
138
231
  console.log('\n' + chalk_1.default.blue('ℹ️ ' + message));
@@ -142,14 +235,23 @@ function displayResults(response) {
142
235
  const notRelevant = results.filter((r) => r.status === 'not_relevant').length;
143
236
  const compliant = results.filter((r) => r.status === 'compliant').length;
144
237
  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`));
238
+ if (showFull) {
239
+ // Show all results when --full flag is used
240
+ if (notRelevant > 0) {
241
+ console.log(chalk_1.default.gray(` ${notRelevant} not relevant`));
242
+ }
243
+ if (compliant > 0) {
244
+ console.log(chalk_1.default.green(` ${compliant} compliant`));
245
+ }
246
+ if (attention > 0) {
247
+ console.log(chalk_1.default.yellow(` ${attention} attention`));
248
+ }
150
249
  }
151
- if (attention > 0) {
152
- console.log(chalk_1.default.yellow(` ${attention} attention`));
250
+ else {
251
+ // Default: only show attention items
252
+ if (attention > 0) {
253
+ console.log(chalk_1.default.yellow(` ${attention} attention`));
254
+ }
153
255
  }
154
256
  if (metadata.timedOut > 0) {
155
257
  console.log(chalk_1.default.yellow(` ${metadata.timedOut} timed out`));
@@ -159,7 +261,7 @@ function displayResults(response) {
159
261
  }
160
262
  console.log('');
161
263
  // Show attention items
162
- const attentionItems = results.filter((r) => r.status === 'attention');
264
+ const attentionItems = filteredResults.filter((r) => r.status === 'attention');
163
265
  if (attentionItems.length > 0) {
164
266
  for (const item of attentionItems) {
165
267
  console.log(chalk_1.default.yellow(`⚠️ ${item.expertId}`));
@@ -176,8 +278,15 @@ function displayResults(response) {
176
278
  }
177
279
  console.log('');
178
280
  }
179
- // Show compliant items (optional, can be verbose)
180
- if (attentionItems.length === 0 && compliant > 0) {
281
+ // Show compliant items (only when --full flag is used)
282
+ if (showFull) {
283
+ const compliantItems = filteredResults.filter((r) => r.status === 'compliant');
284
+ if (compliantItems.length > 0 && attentionItems.length === 0) {
285
+ console.log(chalk_1.default.green('✓ All threadlines passed!\n'));
286
+ }
287
+ }
288
+ else if (attentionItems.length === 0 && compliant > 0) {
289
+ // Default: show success message if no attention items
181
290
  console.log(chalk_1.default.green('✓ All threadlines passed!\n'));
182
291
  }
183
292
  }
@@ -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,125 @@ 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
+ // Try to detect base branch: upstream, default branch, or common names
67
+ base = await detectBaseBranch(git, branchName);
68
+ }
69
+ // Helper function to detect base branch
70
+ async function detectBaseBranch(git, branchName) {
71
+ // Try upstream tracking branch
72
+ const upstream = await git.revparse(['--abbrev-ref', '--symbolic-full-name', `${branchName}@{u}`]).catch(() => null);
73
+ if (upstream) {
74
+ // Extract base from upstream (e.g., "origin/main" -> "main")
75
+ return upstream.replace(/^origin\//, '');
76
+ }
77
+ // Try default branch
78
+ try {
79
+ const defaultBranch = await git.revparse(['--abbrev-ref', 'refs/remotes/origin/HEAD']);
80
+ return defaultBranch.replace(/^origin\//, '');
81
+ }
82
+ catch {
83
+ // Fallback to common names
84
+ const commonBases = ['main', 'master', 'develop'];
85
+ for (const candidate of commonBases) {
86
+ try {
87
+ await git.revparse([`origin/${candidate}`]);
88
+ return candidate;
89
+ }
90
+ catch {
91
+ // Try next
92
+ }
93
+ }
94
+ throw new Error(`Could not determine base branch. Please specify with --base flag or set upstream tracking.`);
95
+ }
96
+ }
97
+ // Get diff between base and branch (cumulative diff of all commits)
98
+ // Format: git diff base...branch (three-dot notation finds common ancestor)
99
+ const diff = await git.diff([`${base}...${branchName}`]);
100
+ // Get list of changed files
101
+ const diffSummary = await git.diffSummary([`${base}...${branchName}`]);
102
+ const changedFiles = diffSummary.files.map(f => f.file);
103
+ return {
104
+ diff: diff || '',
105
+ changedFiles
106
+ };
107
+ }
108
+ /**
109
+ * Get diff for a specific commit
110
+ */
111
+ async function getCommitDiff(repoRoot, sha) {
112
+ const git = (0, simple_git_1.default)(repoRoot);
113
+ // Check if we're in a git repo
114
+ const isRepo = await git.checkIsRepo();
115
+ if (!isRepo) {
116
+ throw new Error('Not a git repository. Threadline requires a git repository.');
117
+ }
118
+ // Get diff for the commit
119
+ // Use git show to get the commit diff
120
+ let diff;
121
+ let changedFiles;
122
+ try {
123
+ // Get diff using git show
124
+ diff = await git.show([sha, '--format=', '--no-color']);
125
+ // Get changed files using git show --name-only
126
+ const commitFiles = await git.show([sha, '--name-only', '--format=', '--pretty=format:']);
127
+ changedFiles = commitFiles
128
+ .split('\n')
129
+ .filter(line => line.trim().length > 0)
130
+ .map(line => line.trim());
131
+ }
132
+ catch (error) {
133
+ // Fallback: try git diff format
134
+ try {
135
+ diff = await git.diff([`${sha}^..${sha}`]);
136
+ // Get files from diff summary
137
+ const diffSummary = await git.diffSummary([`${sha}^..${sha}`]);
138
+ changedFiles = diffSummary.files.map(f => f.file);
139
+ }
140
+ catch (diffError) {
141
+ throw new Error(`Commit ${sha} not found or invalid: ${error.message || diffError.message}`);
142
+ }
143
+ }
144
+ return {
145
+ diff: diff || '',
146
+ changedFiles
147
+ };
148
+ }
149
+ /**
150
+ * Get diff for PR/MR (source branch vs target branch)
151
+ */
152
+ async function getPRMRDiff(repoRoot, sourceBranch, targetBranch) {
153
+ const git = (0, simple_git_1.default)(repoRoot);
154
+ // Check if we're in a git repo
155
+ const isRepo = await git.checkIsRepo();
156
+ if (!isRepo) {
157
+ throw new Error('Not a git repository. Threadline requires a git repository.');
158
+ }
159
+ // Get diff between target and source (cumulative diff)
160
+ // Format: git diff target...source (three-dot notation finds common ancestor)
161
+ const diff = await git.diff([`${targetBranch}...${sourceBranch}`]);
162
+ // Get list of changed files
163
+ const diffSummary = await git.diffSummary([`${targetBranch}...${sourceBranch}`]);
164
+ const changedFiles = diffSummary.files.map(f => f.file);
165
+ return {
166
+ diff: diff || '',
167
+ changedFiles
168
+ };
169
+ }
@@ -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
@@ -62,5 +62,11 @@ program
62
62
  .command('check')
63
63
  .description('Check code against your threadlines')
64
64
  .option('--api-url <url>', 'Threadline server URL', process.env.THREADLINE_API_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.7",
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
  }