threadlines 0.2.6 → 0.2.9
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/api/client.js +2 -17
- package/dist/commands/check.js +26 -41
- package/dist/commands/init.js +10 -1
- package/dist/git/bitbucket.js +23 -92
- package/dist/git/diff.js +0 -196
- package/dist/git/file.js +2 -1
- package/dist/git/github.js +32 -52
- package/dist/git/gitlab.js +23 -49
- package/dist/git/local.js +2 -2
- package/dist/git/vercel.js +2 -2
- package/dist/index.js +18 -7
- package/dist/utils/config-file.js +118 -0
- package/dist/utils/config.js +98 -57
- package/dist/utils/context.js +4 -184
- package/dist/utils/logger.js +65 -0
- package/dist/validators/experts.js +2 -1
- package/package.json +1 -1
- package/dist/git/repo.js +0 -253
package/dist/git/github.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* GitHub Actions Environment
|
|
3
|
+
* GitHub Actions Environment
|
|
4
4
|
*
|
|
5
5
|
* All GitHub-specific logic is contained in this file.
|
|
6
6
|
* No dependencies on other environment implementations.
|
|
@@ -52,10 +52,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
52
52
|
exports.getGitHubContext = getGitHubContext;
|
|
53
53
|
const simple_git_1 = __importDefault(require("simple-git"));
|
|
54
54
|
const fs = __importStar(require("fs"));
|
|
55
|
-
const repo_1 = require("./repo");
|
|
56
55
|
const diff_1 = require("./diff");
|
|
57
56
|
/**
|
|
58
|
-
* Gets all GitHub context
|
|
57
|
+
* Gets all GitHub context
|
|
59
58
|
*/
|
|
60
59
|
async function getGitHubContext(repoRoot) {
|
|
61
60
|
const git = (0, simple_git_1.default)(repoRoot);
|
|
@@ -70,7 +69,6 @@ async function getGitHubContext(repoRoot) {
|
|
|
70
69
|
const branchName = await getBranchName();
|
|
71
70
|
const context = detectContext();
|
|
72
71
|
const commitSha = getCommitSha(context);
|
|
73
|
-
// Get commit author (fails loudly if unavailable)
|
|
74
72
|
// Note: commitSha parameter not needed - GitHub reads from GITHUB_EVENT_PATH JSON
|
|
75
73
|
const commitAuthor = await getCommitAuthor();
|
|
76
74
|
// Get commit message if we have a SHA
|
|
@@ -96,17 +94,24 @@ async function getGitHubContext(repoRoot) {
|
|
|
96
94
|
}
|
|
97
95
|
/**
|
|
98
96
|
* Gets diff for GitHub Actions CI environment
|
|
97
|
+
*
|
|
98
|
+
* Strategy:
|
|
99
|
+
* - PR context: Compare source branch vs target branch (full PR diff)
|
|
100
|
+
* - Any push (main or feature branch): Compare last commit only (HEAD~1...HEAD)
|
|
101
|
+
*
|
|
102
|
+
* Note: Unlike GitLab/Bitbucket, we don't need to fetch branches on-demand here.
|
|
103
|
+
* GitHub Actions' `actions/checkout` automatically fetches both base and head refs
|
|
104
|
+
* for pull_request events, even with the default shallow clone (fetch-depth: 1).
|
|
105
|
+
* The refs `origin/${GITHUB_BASE_REF}` and `origin/${GITHUB_HEAD_REF}` are available
|
|
106
|
+
* immediately after checkout.
|
|
99
107
|
*/
|
|
100
108
|
async function getDiff(repoRoot) {
|
|
101
109
|
const git = (0, simple_git_1.default)(repoRoot);
|
|
102
|
-
const defaultBranch = await (0, repo_1.getDefaultBranchName)(repoRoot);
|
|
103
|
-
// Determine context from GitHub environment variables
|
|
104
110
|
const eventName = process.env.GITHUB_EVENT_NAME;
|
|
105
111
|
const baseRef = process.env.GITHUB_BASE_REF;
|
|
106
112
|
const headRef = process.env.GITHUB_HEAD_REF;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// Scenario 1: PR Context
|
|
113
|
+
// PR Context: Compare source vs target branch
|
|
114
|
+
// No fetch needed - GitHub Actions provides both refs automatically
|
|
110
115
|
if (eventName === 'pull_request') {
|
|
111
116
|
if (!baseRef || !headRef) {
|
|
112
117
|
throw new Error('GitHub PR context detected but GITHUB_BASE_REF or GITHUB_HEAD_REF is missing. ' +
|
|
@@ -120,36 +125,14 @@ async function getDiff(repoRoot) {
|
|
|
120
125
|
changedFiles
|
|
121
126
|
};
|
|
122
127
|
}
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
changedFiles
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
136
|
-
throw new Error(`Could not get diff for default branch '${defaultBranch}'. ` +
|
|
137
|
-
`This might be the first commit on the branch. Error: ${errorMessage}`);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
// Scenario 3: Feature Branch Push
|
|
141
|
-
if (refName) {
|
|
142
|
-
const diff = await git.diff([`origin/${defaultBranch}...origin/${refName}`, '-U200']);
|
|
143
|
-
const diffSummary = await git.diffSummary([`origin/${defaultBranch}...origin/${refName}`]);
|
|
144
|
-
const changedFiles = diffSummary.files.map(f => f.file);
|
|
145
|
-
return {
|
|
146
|
-
diff: diff || '',
|
|
147
|
-
changedFiles
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
throw new Error('GitHub Actions environment detected but no valid context found. ' +
|
|
151
|
-
'Expected GITHUB_EVENT_NAME="pull_request" (with GITHUB_BASE_REF/GITHUB_HEAD_REF) ' +
|
|
152
|
-
'or GITHUB_REF_NAME for branch context.');
|
|
128
|
+
// Any push (main or feature branch): Review last commit only
|
|
129
|
+
const diff = await git.diff(['HEAD~1...HEAD', '-U200']);
|
|
130
|
+
const diffSummary = await git.diffSummary(['HEAD~1...HEAD']);
|
|
131
|
+
const changedFiles = diffSummary.files.map(f => f.file);
|
|
132
|
+
return {
|
|
133
|
+
diff: diff || '',
|
|
134
|
+
changedFiles
|
|
135
|
+
};
|
|
153
136
|
}
|
|
154
137
|
/**
|
|
155
138
|
* Gets repository name for GitHub Actions
|
|
@@ -175,10 +158,13 @@ async function getBranchName() {
|
|
|
175
158
|
return refName;
|
|
176
159
|
}
|
|
177
160
|
/**
|
|
178
|
-
* Detects GitHub context (PR
|
|
161
|
+
* Detects GitHub context (PR or commit)
|
|
162
|
+
*
|
|
163
|
+
* - PR context: When GITHUB_EVENT_NAME is 'pull_request'
|
|
164
|
+
* - Commit context: Any push (main or feature branch) - reviews single commit
|
|
179
165
|
*/
|
|
180
166
|
function detectContext() {
|
|
181
|
-
//
|
|
167
|
+
// PR context
|
|
182
168
|
if (process.env.GITHUB_EVENT_NAME === 'pull_request') {
|
|
183
169
|
const targetBranch = process.env.GITHUB_BASE_REF;
|
|
184
170
|
const sourceBranch = process.env.GITHUB_HEAD_REF;
|
|
@@ -192,22 +178,16 @@ function detectContext() {
|
|
|
192
178
|
};
|
|
193
179
|
}
|
|
194
180
|
}
|
|
195
|
-
//
|
|
196
|
-
if (process.env.GITHUB_REF_NAME) {
|
|
197
|
-
return {
|
|
198
|
-
type: 'branch',
|
|
199
|
-
branchName: process.env.GITHUB_REF_NAME
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
// 3. Check for commit context
|
|
181
|
+
// Any push (main or feature branch) → commit context
|
|
203
182
|
if (process.env.GITHUB_SHA) {
|
|
204
183
|
return {
|
|
205
184
|
type: 'commit',
|
|
206
185
|
commitSha: process.env.GITHUB_SHA
|
|
207
186
|
};
|
|
208
187
|
}
|
|
209
|
-
|
|
210
|
-
|
|
188
|
+
throw new Error('GitHub Actions: Could not detect context. ' +
|
|
189
|
+
'Expected GITHUB_EVENT_NAME="pull_request" or GITHUB_SHA to be set. ' +
|
|
190
|
+
'This should be automatically provided by GitHub Actions.');
|
|
211
191
|
}
|
|
212
192
|
/**
|
|
213
193
|
* Gets commit SHA from context
|
|
@@ -216,7 +196,7 @@ function getCommitSha(context) {
|
|
|
216
196
|
if (context.type === 'commit') {
|
|
217
197
|
return context.commitSha;
|
|
218
198
|
}
|
|
219
|
-
if (context.type === '
|
|
199
|
+
if (context.type === 'pr') {
|
|
220
200
|
return process.env.GITHUB_SHA;
|
|
221
201
|
}
|
|
222
202
|
return undefined;
|
package/dist/git/gitlab.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* GitLab CI Environment
|
|
3
|
+
* GitLab CI Environment
|
|
4
4
|
*
|
|
5
5
|
* All GitLab-specific logic is contained in this file.
|
|
6
6
|
* No dependencies on other environment implementations.
|
|
@@ -19,8 +19,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
19
19
|
exports.getGitLabContext = getGitLabContext;
|
|
20
20
|
const simple_git_1 = __importDefault(require("simple-git"));
|
|
21
21
|
const diff_1 = require("./diff");
|
|
22
|
+
const logger_1 = require("../utils/logger");
|
|
22
23
|
/**
|
|
23
|
-
* Gets all GitLab context
|
|
24
|
+
* Gets all GitLab context
|
|
24
25
|
*/
|
|
25
26
|
async function getGitLabContext(repoRoot) {
|
|
26
27
|
const git = (0, simple_git_1.default)(repoRoot);
|
|
@@ -61,59 +62,35 @@ async function getGitLabContext(repoRoot) {
|
|
|
61
62
|
/**
|
|
62
63
|
* Get diff for GitLab CI environment
|
|
63
64
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
65
|
+
* Strategy:
|
|
66
|
+
* - MR context: Fetch target branch, compare source vs target (full MR diff)
|
|
67
|
+
* - Any push (main or feature branch): Compare last commit only (HEAD~1...HEAD)
|
|
66
68
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* 1. MR Context (CI_MERGE_REQUEST_IID is set):
|
|
70
|
-
* - Fetch target branch, then diff target vs source
|
|
71
|
-
*
|
|
72
|
-
* 2. Feature Branch Push (CI_COMMIT_REF_NAME != CI_DEFAULT_BRANCH):
|
|
73
|
-
* - Fetch default branch, then diff default vs feature
|
|
74
|
-
*
|
|
75
|
-
* 3. Default Branch Push (CI_COMMIT_REF_NAME == CI_DEFAULT_BRANCH):
|
|
76
|
-
* - Use HEAD~1...HEAD (last commit only, no fetch needed)
|
|
69
|
+
* Note: GitLab CI does a shallow clone, so we fetch the target branch for MR context.
|
|
70
|
+
* For regular pushes, HEAD~1...HEAD works without additional fetching.
|
|
77
71
|
*/
|
|
78
72
|
async function getDiff(repoRoot) {
|
|
79
73
|
const git = (0, simple_git_1.default)(repoRoot);
|
|
80
|
-
// Get GitLab CI environment variables
|
|
81
74
|
const mrIid = process.env.CI_MERGE_REQUEST_IID;
|
|
82
75
|
const targetBranch = process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME;
|
|
83
76
|
const sourceBranch = process.env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME;
|
|
84
|
-
|
|
85
|
-
const defaultBranch = process.env.CI_DEFAULT_BRANCH || 'main';
|
|
86
|
-
// Scenario 1: MR Context
|
|
77
|
+
// MR Context: Fetch target branch and compare
|
|
87
78
|
if (mrIid) {
|
|
88
79
|
if (!targetBranch || !sourceBranch) {
|
|
89
80
|
throw new Error('GitLab MR context detected but CI_MERGE_REQUEST_TARGET_BRANCH_NAME or ' +
|
|
90
81
|
'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME is missing. ' +
|
|
91
82
|
'This should be automatically provided by GitLab CI.');
|
|
92
83
|
}
|
|
93
|
-
|
|
84
|
+
logger_1.logger.debug(`Fetching target branch: origin/${targetBranch}`);
|
|
94
85
|
await git.fetch(['origin', `${targetBranch}:refs/remotes/origin/${targetBranch}`, '--depth=1']);
|
|
95
86
|
const diff = await git.diff([`origin/${targetBranch}...origin/${sourceBranch}`, '-U200']);
|
|
96
87
|
const diffSummary = await git.diffSummary([`origin/${targetBranch}...origin/${sourceBranch}`]);
|
|
97
88
|
const changedFiles = diffSummary.files.map(f => f.file);
|
|
98
89
|
return { diff: diff || '', changedFiles };
|
|
99
90
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
// Scenario 3: Default Branch Push
|
|
105
|
-
if (refName === defaultBranch) {
|
|
106
|
-
console.log(` [GitLab] Push to default branch (${defaultBranch}), using HEAD~1...HEAD`);
|
|
107
|
-
const diff = await git.diff(['HEAD~1...HEAD', '-U200']);
|
|
108
|
-
const diffSummary = await git.diffSummary(['HEAD~1...HEAD']);
|
|
109
|
-
const changedFiles = diffSummary.files.map(f => f.file);
|
|
110
|
-
return { diff: diff || '', changedFiles };
|
|
111
|
-
}
|
|
112
|
-
// Scenario 2: Feature Branch Push
|
|
113
|
-
console.log(` [GitLab] Feature branch push, fetching default branch: origin/${defaultBranch}`);
|
|
114
|
-
await git.fetch(['origin', `${defaultBranch}:refs/remotes/origin/${defaultBranch}`, '--depth=1']);
|
|
115
|
-
const diff = await git.diff([`origin/${defaultBranch}...origin/${refName}`, '-U200']);
|
|
116
|
-
const diffSummary = await git.diffSummary([`origin/${defaultBranch}...origin/${refName}`]);
|
|
91
|
+
// Any push (main or feature branch): Review last commit only
|
|
92
|
+
const diff = await git.diff(['HEAD~1...HEAD', '-U200']);
|
|
93
|
+
const diffSummary = await git.diffSummary(['HEAD~1...HEAD']);
|
|
117
94
|
const changedFiles = diffSummary.files.map(f => f.file);
|
|
118
95
|
return { diff: diff || '', changedFiles };
|
|
119
96
|
}
|
|
@@ -140,10 +117,13 @@ async function getBranchName() {
|
|
|
140
117
|
return refName;
|
|
141
118
|
}
|
|
142
119
|
/**
|
|
143
|
-
* Detects GitLab context (MR
|
|
120
|
+
* Detects GitLab context (MR or commit)
|
|
121
|
+
*
|
|
122
|
+
* - MR context: When CI_MERGE_REQUEST_IID is set
|
|
123
|
+
* - Commit context: Any push (main or feature branch) - reviews single commit
|
|
144
124
|
*/
|
|
145
125
|
function detectContext() {
|
|
146
|
-
//
|
|
126
|
+
// MR context
|
|
147
127
|
const mrIid = process.env.CI_MERGE_REQUEST_IID;
|
|
148
128
|
if (mrIid) {
|
|
149
129
|
const targetBranch = process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME;
|
|
@@ -157,22 +137,16 @@ function detectContext() {
|
|
|
157
137
|
};
|
|
158
138
|
}
|
|
159
139
|
}
|
|
160
|
-
//
|
|
161
|
-
if (process.env.CI_COMMIT_REF_NAME) {
|
|
162
|
-
return {
|
|
163
|
-
type: 'branch',
|
|
164
|
-
branchName: process.env.CI_COMMIT_REF_NAME
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
// 3. Check for commit context
|
|
140
|
+
// Any push (main or feature branch) → commit context
|
|
168
141
|
if (process.env.CI_COMMIT_SHA) {
|
|
169
142
|
return {
|
|
170
143
|
type: 'commit',
|
|
171
144
|
commitSha: process.env.CI_COMMIT_SHA
|
|
172
145
|
};
|
|
173
146
|
}
|
|
174
|
-
|
|
175
|
-
|
|
147
|
+
throw new Error('GitLab CI: Could not detect context. ' +
|
|
148
|
+
'Expected CI_MERGE_REQUEST_IID or CI_COMMIT_SHA to be set. ' +
|
|
149
|
+
'This should be automatically provided by GitLab CI.');
|
|
176
150
|
}
|
|
177
151
|
/**
|
|
178
152
|
* Gets commit SHA from context
|
|
@@ -181,7 +155,7 @@ function getCommitSha(context) {
|
|
|
181
155
|
if (context.type === 'commit') {
|
|
182
156
|
return context.commitSha;
|
|
183
157
|
}
|
|
184
|
-
if (context.type === '
|
|
158
|
+
if (context.type === 'mr') {
|
|
185
159
|
return process.env.CI_COMMIT_SHA;
|
|
186
160
|
}
|
|
187
161
|
return undefined;
|
package/dist/git/local.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Local Environment
|
|
3
|
+
* Local Environment
|
|
4
4
|
*
|
|
5
5
|
* All Local-specific logic is contained in this file.
|
|
6
6
|
* No dependencies on other environment implementations.
|
|
@@ -19,7 +19,7 @@ exports.getLocalContext = getLocalContext;
|
|
|
19
19
|
const simple_git_1 = __importDefault(require("simple-git"));
|
|
20
20
|
const diff_1 = require("./diff");
|
|
21
21
|
/**
|
|
22
|
-
* Gets all Local context
|
|
22
|
+
* Gets all Local context
|
|
23
23
|
*/
|
|
24
24
|
async function getLocalContext(repoRoot, commitSha) {
|
|
25
25
|
const git = (0, simple_git_1.default)(repoRoot);
|
package/dist/git/vercel.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Vercel Environment
|
|
3
|
+
* Vercel Environment
|
|
4
4
|
*
|
|
5
5
|
* All Vercel-specific logic is contained in this file.
|
|
6
6
|
* No dependencies on other environment implementations.
|
|
@@ -20,7 +20,7 @@ const simple_git_1 = __importDefault(require("simple-git"));
|
|
|
20
20
|
const child_process_1 = require("child_process");
|
|
21
21
|
const diff_1 = require("./diff");
|
|
22
22
|
/**
|
|
23
|
-
* Gets all Vercel context
|
|
23
|
+
* Gets all Vercel context
|
|
24
24
|
*/
|
|
25
25
|
async function getVercelContext(repoRoot) {
|
|
26
26
|
const git = (0, simple_git_1.default)(repoRoot);
|
package/dist/index.js
CHANGED
|
@@ -49,6 +49,7 @@ if (fs.existsSync(envLocalPath)) {
|
|
|
49
49
|
const commander_1 = require("commander");
|
|
50
50
|
const check_1 = require("./commands/check");
|
|
51
51
|
const init_1 = require("./commands/init");
|
|
52
|
+
const logger_1 = require("./utils/logger");
|
|
52
53
|
const program = new commander_1.Command();
|
|
53
54
|
program
|
|
54
55
|
.name('threadlines')
|
|
@@ -57,29 +58,39 @@ program
|
|
|
57
58
|
program
|
|
58
59
|
.command('init')
|
|
59
60
|
.description('Create a template threadline file to get started')
|
|
60
|
-
.
|
|
61
|
+
.option('--debug', 'Enable debug logging (verbose output)')
|
|
62
|
+
.action((options) => {
|
|
63
|
+
if (options.debug) {
|
|
64
|
+
(0, logger_1.enableDebug)();
|
|
65
|
+
}
|
|
66
|
+
(0, init_1.initCommand)();
|
|
67
|
+
});
|
|
61
68
|
program
|
|
62
69
|
.command('check')
|
|
63
70
|
.description('Check code against your threadlines')
|
|
64
|
-
.option('--api-url <url>', 'Threadline server URL', process.env.THREADLINE_API_URL || 'https://devthreadline.com')
|
|
65
71
|
.option('--full', 'Show all results (compliant, attention, not_relevant). Default: only attention items')
|
|
66
|
-
.option('--branch <name>', 'Review all commits in branch vs base (e.g., --branch feature/new-feature)')
|
|
67
72
|
.option('--commit <ref>', 'Review specific commit. Accepts commit SHA or git reference (e.g., HEAD, HEAD~1, abc123). Example: --commit HEAD')
|
|
68
73
|
.option('--file <path>', 'Review entire file (all lines as additions)')
|
|
69
74
|
.option('--folder <path>', 'Review all files in folder recursively')
|
|
70
75
|
.option('--files <paths...>', 'Review multiple specified files')
|
|
76
|
+
.option('--debug', 'Enable debug logging (verbose output)')
|
|
71
77
|
.addHelpText('after', `
|
|
72
78
|
Examples:
|
|
73
79
|
$ threadlines check # Check staged/unstaged changes (local dev)
|
|
74
80
|
$ threadlines check --commit HEAD # Check latest commit locally
|
|
75
|
-
$ threadlines check --branch main # Check all commits in branch vs base
|
|
76
81
|
$ threadlines check --file src/api.ts # Check entire file
|
|
77
82
|
$ threadlines check --full # Show all results (not just attention items)
|
|
83
|
+
$ threadlines check --debug # Enable verbose debug output
|
|
78
84
|
|
|
79
85
|
Auto-detection in CI:
|
|
80
|
-
-
|
|
81
|
-
-
|
|
86
|
+
- PR/MR context → reviews all changes in the PR/MR
|
|
87
|
+
- Push to any branch → reviews the commit being pushed
|
|
82
88
|
- Local development → reviews staged/unstaged changes
|
|
83
89
|
`)
|
|
84
|
-
.action(
|
|
90
|
+
.action((options) => {
|
|
91
|
+
if (options.debug) {
|
|
92
|
+
(0, logger_1.enableDebug)();
|
|
93
|
+
}
|
|
94
|
+
(0, check_1.checkCommand)(options);
|
|
95
|
+
});
|
|
85
96
|
program.parse();
|
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.DEFAULT_CONFIG = void 0;
|
|
40
|
+
exports.loadConfig = loadConfig;
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const simple_git_1 = __importDefault(require("simple-git"));
|
|
44
|
+
exports.DEFAULT_CONFIG = {
|
|
45
|
+
mode: 'online',
|
|
46
|
+
api_url: 'https://devthreadline.com',
|
|
47
|
+
openai_model: 'gpt-5.2',
|
|
48
|
+
openai_service_tier: 'Flex',
|
|
49
|
+
diff_context_lines: 10,
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Finds the git root directory by walking up from startDir.
|
|
53
|
+
* Returns startDir if not in a git repository.
|
|
54
|
+
*/
|
|
55
|
+
async function findGitRoot(startDir) {
|
|
56
|
+
try {
|
|
57
|
+
const git = (0, simple_git_1.default)(startDir);
|
|
58
|
+
const isRepo = await git.checkIsRepo();
|
|
59
|
+
if (isRepo) {
|
|
60
|
+
return (await git.revparse(['--show-toplevel'])).trim();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Not a git repo or error - return startDir
|
|
65
|
+
}
|
|
66
|
+
return startDir;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Loads configuration from .threadlinerc file.
|
|
70
|
+
*
|
|
71
|
+
* Priority:
|
|
72
|
+
* 1. Built-in defaults
|
|
73
|
+
* 2. .threadlinerc file (if exists) - merged with defaults
|
|
74
|
+
*
|
|
75
|
+
* Searches for .threadlinerc starting from startDir, walking up to git root.
|
|
76
|
+
* If no file found, returns defaults.
|
|
77
|
+
*/
|
|
78
|
+
async function loadConfig(startDir) {
|
|
79
|
+
// Start with defaults
|
|
80
|
+
const config = { ...exports.DEFAULT_CONFIG };
|
|
81
|
+
// Find git root to limit search scope
|
|
82
|
+
const gitRoot = await findGitRoot(startDir);
|
|
83
|
+
// Look for .threadlinerc starting from startDir, up to git root
|
|
84
|
+
let currentDir = startDir;
|
|
85
|
+
let configPath = null;
|
|
86
|
+
while (true) {
|
|
87
|
+
const candidatePath = path.join(currentDir, '.threadlinerc');
|
|
88
|
+
if (fs.existsSync(candidatePath)) {
|
|
89
|
+
configPath = candidatePath;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
// Stop at git root
|
|
93
|
+
if (path.resolve(currentDir) === path.resolve(gitRoot)) {
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
// Move up one directory
|
|
97
|
+
const parentDir = path.dirname(currentDir);
|
|
98
|
+
if (parentDir === currentDir) {
|
|
99
|
+
// Reached filesystem root
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
currentDir = parentDir;
|
|
103
|
+
}
|
|
104
|
+
// If config file found, parse and merge
|
|
105
|
+
if (configPath) {
|
|
106
|
+
try {
|
|
107
|
+
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
108
|
+
const fileConfig = JSON.parse(configContent);
|
|
109
|
+
// Merge file config into defaults (file overrides defaults)
|
|
110
|
+
Object.assign(config, fileConfig);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
// If file exists but can't be parsed, log warning but continue with defaults
|
|
114
|
+
console.warn(`Warning: Failed to parse .threadlinerc at ${configPath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return config;
|
|
118
|
+
}
|
package/dist/utils/config.js
CHANGED
|
@@ -1,74 +1,115 @@
|
|
|
1
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
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
4
|
};
|
|
38
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
6
|
exports.getThreadlineApiKey = getThreadlineApiKey;
|
|
40
7
|
exports.getThreadlineAccount = getThreadlineAccount;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const projectRoot = process.cwd();
|
|
50
|
-
const envLocalPath = path.join(projectRoot, '.env.local');
|
|
51
|
-
if (fs.existsSync(envLocalPath)) {
|
|
52
|
-
dotenv_1.default.config({ path: envLocalPath });
|
|
53
|
-
}
|
|
54
|
-
}
|
|
8
|
+
exports.getOpenAIConfig = getOpenAIConfig;
|
|
9
|
+
exports.logOpenAIConfig = logOpenAIConfig;
|
|
10
|
+
exports.isDirectModeAvailable = isDirectModeAvailable;
|
|
11
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
12
|
+
const logger_1 = require("./logger");
|
|
13
|
+
// Default values for OpenAI configuration
|
|
14
|
+
const OPENAI_MODEL_DEFAULT = 'gpt-5.2';
|
|
15
|
+
const OPENAI_SERVICE_TIER_DEFAULT = 'Flex';
|
|
55
16
|
/**
|
|
56
17
|
* Gets THREADLINE_API_KEY from environment.
|
|
57
|
-
*
|
|
18
|
+
*
|
|
19
|
+
* Note: .env.local is automatically loaded at CLI startup (see index.ts).
|
|
20
|
+
* In CI/CD, environment variables are injected directly into process.env.
|
|
58
21
|
*/
|
|
59
22
|
function getThreadlineApiKey() {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
23
|
+
const apiKey = process.env.THREADLINE_API_KEY;
|
|
24
|
+
if (apiKey) {
|
|
25
|
+
logger_1.logger.debug('THREADLINE_API_KEY: found (value hidden for security)');
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
logger_1.logger.debug('THREADLINE_API_KEY: not set');
|
|
29
|
+
}
|
|
30
|
+
return apiKey;
|
|
64
31
|
}
|
|
65
32
|
/**
|
|
66
33
|
* Gets THREADLINE_ACCOUNT from environment.
|
|
67
|
-
*
|
|
34
|
+
*
|
|
35
|
+
* Note: .env.local is automatically loaded at CLI startup (see index.ts).
|
|
36
|
+
* In CI/CD, environment variables are injected directly into process.env.
|
|
68
37
|
*/
|
|
69
38
|
function getThreadlineAccount() {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
39
|
+
const account = process.env.THREADLINE_ACCOUNT;
|
|
40
|
+
if (account) {
|
|
41
|
+
logger_1.logger.debug(`THREADLINE_ACCOUNT: ${account}`);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
logger_1.logger.debug('THREADLINE_ACCOUNT: not set');
|
|
45
|
+
}
|
|
46
|
+
return account;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Gets OpenAI configuration from environment variables.
|
|
50
|
+
*
|
|
51
|
+
* Required:
|
|
52
|
+
* - OPENAI_API_KEY: Your OpenAI API key
|
|
53
|
+
*
|
|
54
|
+
* Optional (with defaults):
|
|
55
|
+
* - OPENAI_MODEL: Model to use (default: gpt-5.2)
|
|
56
|
+
* - OPENAI_SERVICE_TIER: Service tier (default: Flex)
|
|
57
|
+
*
|
|
58
|
+
* Returns undefined if OPENAI_API_KEY is not set.
|
|
59
|
+
*
|
|
60
|
+
* Note: .env.local is automatically loaded at CLI startup (see index.ts).
|
|
61
|
+
* In CI/CD, environment variables are injected directly into process.env.
|
|
62
|
+
*/
|
|
63
|
+
function getOpenAIConfig() {
|
|
64
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
65
|
+
if (!apiKey) {
|
|
66
|
+
logger_1.logger.debug('OPENAI_API_KEY: not set (direct mode unavailable)');
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
logger_1.logger.debug('OPENAI_API_KEY: found (value hidden for security)');
|
|
70
|
+
const model = process.env.OPENAI_MODEL || OPENAI_MODEL_DEFAULT;
|
|
71
|
+
const serviceTier = process.env.OPENAI_SERVICE_TIER || OPENAI_SERVICE_TIER_DEFAULT;
|
|
72
|
+
if (process.env.OPENAI_MODEL) {
|
|
73
|
+
logger_1.logger.debug(`OPENAI_MODEL: ${model} (from environment)`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
logger_1.logger.debug(`OPENAI_MODEL: ${model} (using default)`);
|
|
77
|
+
}
|
|
78
|
+
if (process.env.OPENAI_SERVICE_TIER) {
|
|
79
|
+
logger_1.logger.debug(`OPENAI_SERVICE_TIER: ${serviceTier} (from environment)`);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
logger_1.logger.debug(`OPENAI_SERVICE_TIER: ${serviceTier} (using default)`);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
apiKey,
|
|
86
|
+
model,
|
|
87
|
+
serviceTier
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Logs the OpenAI configuration being used.
|
|
92
|
+
* Call this when starting direct LLM mode to inform the user.
|
|
93
|
+
*/
|
|
94
|
+
function logOpenAIConfig(config) {
|
|
95
|
+
console.log(chalk_1.default.blue('OpenAI Direct Mode:'));
|
|
96
|
+
console.log(chalk_1.default.gray(` Model: ${config.model}${config.model === OPENAI_MODEL_DEFAULT ? ' (default)' : ''}`));
|
|
97
|
+
console.log(chalk_1.default.gray(` Service Tier: ${config.serviceTier}${config.serviceTier === OPENAI_SERVICE_TIER_DEFAULT ? ' (default)' : ''}`));
|
|
98
|
+
console.log('');
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Checks if direct OpenAI mode is available (OPENAI_API_KEY is set).
|
|
102
|
+
*
|
|
103
|
+
* Note: .env.local is automatically loaded at CLI startup (see index.ts).
|
|
104
|
+
* In CI/CD, environment variables are injected directly into process.env.
|
|
105
|
+
*/
|
|
106
|
+
function isDirectModeAvailable() {
|
|
107
|
+
const available = !!process.env.OPENAI_API_KEY;
|
|
108
|
+
if (available) {
|
|
109
|
+
logger_1.logger.debug('Direct OpenAI mode: available (OPENAI_API_KEY found)');
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
logger_1.logger.debug('Direct OpenAI mode: unavailable (OPENAI_API_KEY not set)');
|
|
113
|
+
}
|
|
114
|
+
return available;
|
|
74
115
|
}
|