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.
- package/dist/commands/check.js +128 -19
- package/dist/commands/init.js +3 -2
- package/dist/git/diff.js +128 -0
- package/dist/git/file.js +159 -0
- package/dist/git/repo.js +58 -0
- package/dist/index.js +6 -0
- package/dist/utils/ci-detection.js +56 -0
- package/dist/utils/config.js +11 -0
- package/package.json +8 -6
package/dist/commands/check.js
CHANGED
|
@@ -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.
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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 (
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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 =
|
|
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 (
|
|
180
|
-
if (
|
|
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
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -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:
|
|
99
|
-
console.log(chalk_1.default.white(' To use threadlines check, you need
|
|
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
|
+
}
|
package/dist/git/file.js
ADDED
|
@@ -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
|
+
}
|
package/dist/git/repo.js
ADDED
|
@@ -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
|
+
}
|
package/dist/utils/config.js
CHANGED
|
@@ -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.
|
|
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
|
-
"
|
|
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/
|
|
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
|
}
|